Skip to content

Commit 255477e

Browse files
authored
Merge pull request #2228 from BishopFox/fix/mcp
Fix/mcp
2 parents 14d68d6 + 0acc691 commit 255477e

33 files changed

Lines changed: 5537 additions & 1004 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/crush
22

3+
/examples
4+
35
/.vscode
46
/.vscode/*
57
/.devcontainer

client/command/ai/ai.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
2525
defer con.RemoveEventListener(listenerID)
2626

2727
model := newAIModel(con, buildAIContext(con), listener)
28+
model.showExperimentalWarningModal()
2829

2930
width, height := 100, 30
3031
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
@@ -48,13 +49,15 @@ type aiContext struct {
4849
}
4950

5051
type aiTargetSummary struct {
51-
Label string
52-
Host string
53-
OS string
54-
Arch string
55-
C2 string
56-
Mode string
57-
Details []string
52+
SessionID string
53+
BeaconID string
54+
Label string
55+
Host string
56+
OS string
57+
Arch string
58+
C2 string
59+
Mode string
60+
Details []string
5861
}
5962

6063
type aiConnectionSummary struct {
@@ -97,12 +100,13 @@ func buildAIContext(con *console.SliverClient) aiContext {
97100
switch {
98101
case session != nil:
99102
ctx.target = aiTargetSummary{
100-
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
101-
Host: fallback(session.Hostname, "<unknown host>"),
102-
OS: fallback(session.OS, "unknown"),
103-
Arch: fallback(session.Arch, "unknown"),
104-
C2: fallback(session.ActiveC2, "unknown"),
105-
Mode: "interactive session",
103+
SessionID: session.ID,
104+
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
105+
Host: fallback(session.Hostname, "<unknown host>"),
106+
OS: fallback(session.OS, "unknown"),
107+
Arch: fallback(session.Arch, "unknown"),
108+
C2: fallback(session.ActiveC2, "unknown"),
109+
Mode: "interactive session",
106110
Details: []string{
107111
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
108112
fmt.Sprintf("PID: %d", session.PID),
@@ -111,12 +115,13 @@ func buildAIContext(con *console.SliverClient) aiContext {
111115
}
112116
case beacon != nil:
113117
ctx.target = aiTargetSummary{
114-
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
115-
Host: fallback(beacon.Hostname, "<unknown host>"),
116-
OS: fallback(beacon.OS, "unknown"),
117-
Arch: fallback(beacon.Arch, "unknown"),
118-
C2: fallback(beacon.ActiveC2, "unknown"),
119-
Mode: "asynchronous beacon",
118+
BeaconID: beacon.ID,
119+
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
120+
Host: fallback(beacon.Hostname, "<unknown host>"),
121+
OS: fallback(beacon.OS, "unknown"),
122+
Arch: fallback(beacon.Arch, "unknown"),
123+
C2: fallback(beacon.ActiveC2, "unknown"),
124+
Mode: "asynchronous beacon",
120125
Details: []string{
121126
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
122127
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),

client/command/ai/grpc_test.go

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
4949
rpcClient, cleanup := newAITestRPCClient(t, server)
5050
defer cleanup()
5151

52-
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, "")
52+
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")
5353
msg := cmd()
5454

5555
loaded, ok := msg.(aiLoadedMsg)
@@ -112,7 +112,7 @@ func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
112112
rpcClient, cleanup := newAITestRPCClient(t, server)
113113
defer cleanup()
114114

115-
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, "")()
115+
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")()
116116

117117
loaded, ok := msg.(aiLoadedMsg)
118118
if !ok {
@@ -157,6 +157,7 @@ func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
157157

158158
msg := submitPromptCmd(
159159
&console.SliverClient{Rpc: rpcClient},
160+
aiTargetSummary{SessionID: "session-1"},
160161
nil,
161162
"openai",
162163
"gpt-test",
@@ -179,6 +180,9 @@ func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
179180
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
180181
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
181182
}
183+
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
184+
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
185+
}
182186
if len(server.saveMessageReqs) != 1 {
183187
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
184188
}
@@ -206,6 +210,7 @@ func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
206210

207211
msg := submitPromptCmd(
208212
&console.SliverClient{Rpc: rpcClient},
213+
aiTargetSummary{},
209214
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
210215
"openai",
211216
"gpt-test",
@@ -262,10 +267,16 @@ func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
262267
}
263268

264269
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
265-
conversation := &clientpb.AIConversation{ID: "conv-1", OperatorName: "alice"}
266-
data, err := proto.Marshal(conversation)
270+
eventPayload := &clientpb.AIConversationEvent{
271+
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
272+
Conversation: &clientpb.AIConversation{
273+
ID: "conv-1",
274+
OperatorName: "alice",
275+
},
276+
}
277+
data, err := proto.Marshal(eventPayload)
267278
if err != nil {
268-
t.Fatalf("marshal conversation: %v", err)
279+
t.Fatalf("marshal conversation event: %v", err)
269280
}
270281

271282
listener := make(chan *clientpb.Event, 2)
@@ -278,8 +289,8 @@ func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
278289
if !ok {
279290
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
280291
}
281-
if aiEvent.conversation == nil || aiEvent.conversation.GetID() != "conv-1" {
282-
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.conversation)
292+
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
293+
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
283294
}
284295
}
285296

@@ -293,33 +304,38 @@ func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
293304
}
294305
}
295306

296-
func TestAIModelReloadsSharedConversationEvents(t *testing.T) {
307+
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
297308
listener := make(chan *clientpb.Event)
298309
close(listener)
299310

300311
model := newAIModel(nil, aiContext{
301312
connection: aiConnectionSummary{Operator: "alice"},
302313
}, listener)
303314
model.loading = false
304-
305-
updated, cmd := model.Update(aiConversationEventMsg{conversation: &clientpb.AIConversation{
306-
ID: "conv-2",
307-
OperatorName: "bob",
315+
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
316+
317+
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
318+
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
319+
Conversation: &clientpb.AIConversation{
320+
ID: "conv-2",
321+
OperatorName: "bob",
322+
Title: "Shared thread",
323+
},
308324
}})
309325

310326
nextModel := updated.(*aiModel)
311-
if !nextModel.loading {
312-
t.Fatal("expected shared conversation event to trigger a reload")
327+
if nextModel.loading {
328+
t.Fatal("did not expect shared conversation event to trigger a reload")
313329
}
314-
if nextModel.status != "Conversation updated on the server. Refreshing..." {
315-
t.Fatalf("unexpected reload status: %q", nextModel.status)
330+
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
331+
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
316332
}
317333
if cmd == nil {
318-
t.Fatal("expected shared conversation event to schedule a refresh")
334+
t.Fatal("expected shared conversation event to continue listening for events")
319335
}
320336
}
321337

322-
func TestAIModelReloadsDeleteEventsWhileAwaitingResponse(t *testing.T) {
338+
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
323339
listener := make(chan *clientpb.Event)
324340
close(listener)
325341

@@ -332,22 +348,36 @@ func TestAIModelReloadsDeleteEventsWhileAwaitingResponse(t *testing.T) {
332348
ID: "conv-2",
333349
OperatorName: "alice",
334350
UpdatedAt: 100,
351+
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
335352
Messages: []*clientpb.AIConversationMessage{
336353
{Role: "user", Content: "still waiting"},
337354
},
338355
}
356+
model.conversations = []*clientpb.AIConversation{
357+
{ID: "conv-2", Title: "Current"},
358+
{ID: "conv-1", Title: "Fallback"},
359+
}
339360

340-
updated, cmd := model.Update(aiConversationEventMsg{conversation: &clientpb.AIConversation{
341-
ID: "conv-2",
342-
OperatorName: "bob",
361+
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
362+
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
363+
ConversationID: "conv-2",
364+
Conversation: &clientpb.AIConversation{
365+
ID: "conv-2",
366+
},
343367
}})
344368

345369
nextModel := updated.(*aiModel)
346-
if !nextModel.loading {
347-
t.Fatal("expected delete tombstone to reload the conversation")
370+
if nextModel.loading {
371+
t.Fatal("did not expect delete tombstone to trigger a reload")
372+
}
373+
if nextModel.currentConversation != nil {
374+
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
375+
}
376+
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
377+
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
348378
}
349379
if cmd == nil {
350-
t.Fatal("expected delete tombstone to schedule a refresh")
380+
t.Fatal("expected delete tombstone to continue listening for events")
351381
}
352382
}
353383

0 commit comments

Comments
 (0)