Skip to content

Commit d2f83d6

Browse files
committed
feat(assistant): show and manage pending command state
1 parent 721792b commit d2f83d6

2 files changed

Lines changed: 116 additions & 3 deletions

File tree

C2Client/C2Client/AssistantPanel.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,18 @@ def consoleAssistantMethod(self, action, beaconHash, listenerHash, context, cmd,
132132

133133
if awaiting_result:
134134
pending_id = self.pending_tool_id
135+
tool_output = self._format_tool_result_for_resume(
136+
beacon_hash=beaconHash,
137+
listener_hash=listenerHash,
138+
command_id=commandId,
139+
command=command_text,
140+
output=display_output,
141+
)
142+
self.printInTerminal("Analysis:", f"Received result for command `{commandId or command_text}`. Resuming assistant.")
135143
self._clear_pending_tool_state()
136144

137145
if pending_id:
138-
self._start_agent_resume(pending_id, display_output)
146+
self._start_agent_resume(pending_id, tool_output)
139147
else:
140148
combined = command_text
141149
if output_text:
@@ -149,6 +157,20 @@ def consoleAssistantMethod(self, action, beaconHash, listenerHash, context, cmd,
149157
output=output_text,
150158
)
151159

160+
161+
def _format_tool_result_for_resume(self, *, beacon_hash, listener_hash, command_id, command, output):
162+
return "\n".join(
163+
[
164+
"Command result received from TeamServer.",
165+
f"command_id: {command_id or 'unknown'}",
166+
f"beacon_hash: {beacon_hash or 'unknown'}",
167+
f"listener_hash: {listener_hash or 'unknown'}",
168+
f"command: {command or 'unknown'}",
169+
"output:",
170+
output or "[no output]",
171+
]
172+
)
173+
152174
def event(self, event):
153175
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab:
154176
self.tabPressed.emit()
@@ -193,7 +215,15 @@ def runCommand(self):
193215
self._show_local_help()
194216
return
195217

218+
if local_command == "/status":
219+
self._show_pending_status()
220+
return
221+
196222
if local_command in {"/cancel", "/reset"}:
223+
if not self.awaiting_tool_result:
224+
self.printInTerminal("Analysis:", "No pending command wait to cancel.")
225+
return
226+
197227
self._clear_pending_tool_state()
198228
self.printInTerminal("Analysis:", "Pending command wait cancelled.")
199229
return
@@ -228,6 +258,7 @@ def _show_local_help(self):
228258
"\n".join(
229259
[
230260
"/help - Show AssistantPanel local commands.",
261+
"/status - Show the current assistant pending command state.",
231262
"/cancel - Cancel the current pending beacon result wait.",
232263
"/reset - Alias for /cancel.",
233264
timeout_line,
@@ -236,6 +267,44 @@ def _show_local_help(self):
236267
)
237268

238269

270+
def _show_pending_status(self):
271+
self.printInTerminal("Analysis:", self._format_pending_status())
272+
273+
274+
def _format_pending_status(self, prefix=None):
275+
if not self.awaiting_tool_result:
276+
with self._response_lock:
277+
busy = self._response_thread is not None and self._response_thread.is_alive()
278+
if busy:
279+
return "Assistant is processing a request. No beacon command result is pending yet."
280+
return "No pending beacon command result."
281+
282+
context = self.pending_tool_context or {}
283+
command = context.get("command_line") or "unknown command"
284+
command_id = context.get("command_id") or "unknown"
285+
beacon_hash = context.get("beacon_hash") or "unknown"
286+
listener_hash = context.get("listener_hash") or "unknown"
287+
288+
lines = []
289+
if prefix:
290+
lines.append(prefix)
291+
lines.extend(
292+
[
293+
f"Pending command: {command}",
294+
f"Command ID: {command_id}",
295+
f"Beacon: {beacon_hash}",
296+
f"Listener: {listener_hash}",
297+
]
298+
)
299+
300+
if self.pending_tool_timeout_ms > 0:
301+
lines.append(f"Timeout: {self.pending_tool_timeout_ms // 1000}s")
302+
else:
303+
lines.append("Timeout: disabled")
304+
305+
return "\n".join(lines)
306+
307+
239308
def _start_agent_turn(self, user_input):
240309
with self._response_lock:
241310
if self._response_thread and self._response_thread.is_alive():
@@ -303,6 +372,7 @@ def _process_assistant_response(self, message):
303372
"command_line": metadata.get("command_line") or arguments.get("command_line"),
304373
}
305374
self._start_pending_tool_timer()
375+
self.printInTerminal("Analysis:", self._format_pending_status("Waiting for beacon command result."))
306376
else:
307377
self._stop_pending_tool_timer()
308378

@@ -326,13 +396,15 @@ def _handle_pending_tool_timeout(self):
326396

327397
context = self.pending_tool_context or {}
328398
command = context.get("command_line") or context.get("command_id") or "command"
399+
command_id = context.get("command_id")
329400
beacon_hash = context.get("beacon_hash")
330401
target = f" on beacon {beacon_hash[:8]}" if beacon_hash else ""
402+
command_id_text = f" Command ID: {command_id}." if command_id else ""
331403

332404
self._clear_pending_tool_state()
333405
self.printInTerminal(
334406
"Analysis:",
335-
f"Timed out waiting for result of `{command}`{target}. The assistant is ready for a new request.",
407+
f"Timed out waiting for result of `{command}`{target}.{command_id_text} The assistant is ready for a new request.",
336408
)
337409

338410
def _handle_assistant_error(self, error_message):

C2Client/tests/test_assistant_panel.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def test_help_command_shows_local_commands(qtbot, monkeypatch):
6666
output = assistant.editorOutput.toPlainText()
6767
assert "Assistant commands:" in output
6868
assert "/help - Show AssistantPanel local commands." in output
69+
assert "/status - Show the current assistant pending command state." in output
6970
assert "/cancel - Cancel the current pending beacon result wait." in output
7071
assert "/reset - Alias for /cancel." in output
7172

@@ -96,6 +97,15 @@ def test_cancel_command_clears_pending_state(qtbot, monkeypatch):
9697
assert "Pending command wait cancelled." in assistant.editorOutput.toPlainText()
9798

9899

100+
def test_cancel_command_reports_when_nothing_is_pending(qtbot, monkeypatch):
101+
assistant = build_assistant(qtbot, monkeypatch)
102+
103+
assistant.commandEditor.setText("/cancel")
104+
assistant.runCommand()
105+
106+
assert "No pending command wait to cancel." in assistant.editorOutput.toPlainText()
107+
108+
99109
def test_help_command_is_available_while_pending(qtbot, monkeypatch):
100110
assistant = build_assistant(qtbot, monkeypatch)
101111
assistant.awaiting_tool_result = True
@@ -114,6 +124,29 @@ def test_help_command_is_available_while_pending(qtbot, monkeypatch):
114124
assert "Assistant commands:" in assistant.editorOutput.toPlainText()
115125

116126

127+
def test_status_command_reports_no_pending_state(qtbot, monkeypatch):
128+
assistant = build_assistant(qtbot, monkeypatch)
129+
130+
assistant.commandEditor.setText("/status")
131+
assistant.runCommand()
132+
133+
assert "No pending beacon command result." in assistant.editorOutput.toPlainText()
134+
135+
136+
def test_status_command_reports_pending_context(qtbot, monkeypatch):
137+
assistant = build_assistant(qtbot, monkeypatch)
138+
assistant._process_assistant_response(pending_message())
139+
140+
assistant.commandEditor.setText("/status")
141+
assistant.runCommand()
142+
143+
output = assistant.editorOutput.toPlainText()
144+
assert "Pending command: run touch /tmp/hello" in output
145+
assert "Command ID: cmd-1" in output
146+
assert "Beacon: beacon-12345678" in output
147+
assert "Listener: listener-1" in output
148+
149+
117150
def test_console_receive_resumes_matching_pending_command_id(qtbot, monkeypatch):
118151
assistant = build_assistant(qtbot, monkeypatch)
119152
resume_calls = []
@@ -140,7 +173,13 @@ def test_console_receive_resumes_matching_pending_command_id(qtbot, monkeypatch)
140173
"cmd-1",
141174
)
142175

143-
assert resume_calls == [("pending-1", "[no output]")]
176+
assert len(resume_calls) == 1
177+
assert resume_calls[0][0] == "pending-1"
178+
assert "command_id: cmd-1" in resume_calls[0][1]
179+
assert "beacon_hash: beacon-1" in resume_calls[0][1]
180+
assert "listener_hash: listener-1" in resume_calls[0][1]
181+
assert "command: run touch /tmp/hello" in resume_calls[0][1]
182+
assert "[no output]" in resume_calls[0][1]
144183
assert assistant.awaiting_tool_result is False
145184
assert assistant.pending_tool_id is None
146185
assert assistant.pending_tool_context is None
@@ -195,6 +234,8 @@ def test_pending_response_starts_timeout_timer(qtbot, monkeypatch):
195234
assert assistant.pending_tool_context["command_id"] == "cmd-1"
196235
assert assistant.pending_tool_context["command_line"] == "run touch /tmp/hello"
197236
assert assistant.pending_tool_timer.isActive() is True
237+
assert "Waiting for beacon command result." in assistant.editorOutput.toPlainText()
238+
assert "Command ID: cmd-1" in assistant.editorOutput.toPlainText()
198239

199240

200241
def test_pending_timeout_clears_state_and_reports(qtbot, monkeypatch):

0 commit comments

Comments
 (0)