Skip to content
Merged

Ai/tui #2221

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fa0e13a
Basic tui layout
moloch-- Mar 14, 2026
b78915f
Basic db layout
moloch-- Mar 14, 2026
1f1b759
Merge branch 'codex/add-bubbletea-ai-tui-layout' into ai/tui
moloch-- Mar 14, 2026
cd523c7
Wire tui to ai grpc
moloch-- Mar 14, 2026
3ce6d69
Add server-side ai-config form
moloch-- Mar 14, 2026
635ee42
Add animation
moloch-- Mar 14, 2026
fffe938
Improve server-side ai grpc handlers
moloch-- Mar 14, 2026
9f6bfb6
Working animation
moloch-- Mar 14, 2026
613c030
Add markdown rendering
moloch-- Mar 14, 2026
a4c8968
Resize tui on window resize
moloch-- Mar 15, 2026
266dabf
Redesign modals
moloch-- Mar 15, 2026
4a5ad6e
Redesign delete modal
moloch-- Mar 15, 2026
6d7a10b
Switch to fantasy to call models
moloch-- Mar 15, 2026
eeef542
Make conversation scrollable
moloch-- Mar 15, 2026
99ec812
Improve scroll of conversation pane, move context info to modal
moloch-- Mar 15, 2026
f967a32
Add scrollbar
moloch-- Mar 15, 2026
aa02293
Fix borders
moloch-- Mar 15, 2026
8e8398b
Tweak border
moloch-- Mar 15, 2026
f003468
Tweak tui borders
moloch-- Mar 15, 2026
2c025d5
Add new conversation modal
moloch-- Mar 15, 2026
253f1a8
Create docs go pkg
moloch-- Mar 15, 2026
d231943
Create docs go pkg
moloch-- Mar 15, 2026
2ca95f7
Add docs command
moloch-- Mar 15, 2026
3a93f31
Add optional Wireguard multiplayer
moloch-- Mar 15, 2026
682b3c2
Upgrade to charm v2 libs
moloch-- Mar 15, 2026
45dc3ea
Improve doc file selector
moloch-- Mar 15, 2026
88176df
Improve docs command
moloch-- Mar 15, 2026
f56886b
Merge grpc/mtls+wg branch
moloch-- Mar 15, 2026
01d9b21
fix grpc/mtls+wg for multiplayer
moloch-- Mar 15, 2026
b1ee035
isolate multiplayer wg from c2 wg
moloch-- Mar 15, 2026
4622c50
Add ai chat sync
moloch-- Mar 15, 2026
9ef54ca
Add mouse support to ai tui
moloch-- Mar 15, 2026
a7edd98
Sync prompt to stdout
moloch-- Mar 15, 2026
4d551b7
Fix huh form rendering
moloch-- Mar 15, 2026
4051088
Update gh action workflows to 1.26.1
moloch-- Mar 15, 2026
e02b03c
Go 1.25.7
moloch-- Mar 16, 2026
93fdfb3
Merge branch 'master' into ai/tui
moloch-- Mar 16, 2026
8bd0257
Go 1.25.8
moloch-- Mar 16, 2026
14e9121
Go 1.25.8
moloch-- Mar 16, 2026
4949821
Fix WireGuard generate test IP collisions
moloch-- Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/workflows/autorelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ jobs:
with:
fetch-depth: 0

- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-scanning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:

steps:
# Setup go environment
- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: Checkout repository
uses: actions/checkout@v6
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: OS Packages
run: |
Expand Down Expand Up @@ -71,10 +71,10 @@ jobs:
runs-on: windows-latest
timeout-minutes: 180
steps:
- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: Install make
run: choco install make -y --no-progress
Expand Down Expand Up @@ -115,10 +115,10 @@ jobs:
runs-on: macos-latest
timeout-minutes: 180
steps:
- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: Check Out Code
uses: actions/checkout@v6
Expand Down Expand Up @@ -153,10 +153,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.25
- name: Go 1.25.8
uses: actions/setup-go@v6
with:
go-version: "1.25.6"
go-version: "1.25.8"

- name: Check Out Code
uses: actions/checkout@v6
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/crush

/.vscode
/.vscode/*
/.devcontainer
Expand Down
24 changes: 17 additions & 7 deletions client/assets/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,25 @@ const (
ConfigDirName = "configs"
)

// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
type ClientWGConfig struct {
ServerPubKey string `json:"server_pub_key"`
ClientPrivateKey string `json:"client_private_key"`
ClientPubKey string `json:"client_pub_key"`
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
}

// ClientConfig - Client JSON config
type ClientConfig struct {
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
WG *ClientWGConfig `json:"wg,omitempty"`
}

// GetConfigDir - Returns the path to the config dir
Expand Down
2 changes: 2 additions & 0 deletions client/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func init() {

rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "connect to multiplayer directly even if the operator config includes a WireGuard wrapper")
rootCmd.PersistentFlags().Bool(requireWGFlag, false, "require the operator config's WireGuard wrapper for multiplayer connections")

// Create the console client, without any RPC or commands bound to it yet.
// This created before anything so that multiple commands can make use of
Expand Down
3 changes: 3 additions & 0 deletions client/cli/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {

func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
pre = func(cmd *cobra.Command, _ []string) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}

configs := assets.GetConfigs()
if len(configs) == 0 {
Expand Down
6 changes: 5 additions & 1 deletion client/cli/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
}

func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}

configPath, _ := cmd.Flags().GetString("config")
config, err := loadMCPClientConfig(configPath)
if err != nil {
Expand All @@ -55,7 +59,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
fmt.Printf("Connection to server failed %s\n", err)
return nil
}
defer ln.Close()
defer transport.CloseGRPCConnection(ln)

con.Rpc = rpc
go handleConnectionLost(ln)
Expand Down
42 changes: 42 additions & 0 deletions client/cli/transport_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"fmt"

"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)

const (
requireWGFlag = "require-wg"
disableWGFlag = "disable-wg"
)

func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if cmd == nil {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
return nil
}

requireWG, err := cmd.Flags().GetBool(requireWGFlag)
if err != nil {
return err
}
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
if err != nil {
return err
}
if requireWG && disableWG {
return fmt.Errorf("--%s and --%s cannot be used together", requireWGFlag, disableWGFlag)
}

mode := transport.MultiplayerConnectAuto
switch {
case requireWG:
mode = transport.MultiplayerConnectRequireWG
case disableWG:
mode = transport.MultiplayerConnectDisableWG
}
transport.SetMultiplayerConnectMode(mode)
return nil
}
1 change: 1 addition & 0 deletions client/command/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com

## Sub-packages

- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
- `alias/` – Implements the 'alias' command group for the Sliver client console.
- `armory/` – Implements the 'armory' command group for the Sliver client console.
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
Expand Down
5 changes: 5 additions & 0 deletions client/command/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# client/command/ai

Implements the `ai` command for the Sliver client console.

The TUI loads server-backed AI conversation threads over gRPC, submits prompts to the server, and refreshes when `AIConversationEvent` updates arrive with assistant replies or failure messages.
145 changes: 145 additions & 0 deletions client/command/ai/ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package ai

import (
"fmt"
"strings"
"time"

tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
)

// AICmd launches the AI conversation TUI.
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
if con == nil || con.Rpc == nil {
if con != nil {
con.PrintErrorf("Connect to a server before using `ai`.\n")
}
return
}

listenerID, listener := con.CreateEventListener()
defer con.RemoveEventListener(listenerID)

model := newAIModel(con, buildAIContext(con), listener)

width, height := 100, 30
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}

program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
if _, err := program.Run(); err != nil {
con.PrintErrorf("AI TUI error: %s\n", err)
}
}

type aiContext struct {
target aiTargetSummary
connection aiConnectionSummary
status string
}

type aiTargetSummary struct {
Label string
Host string
OS string
Arch string
C2 string
Mode string
Details []string
}

type aiConnectionSummary struct {
Profile string
Server string
Operator string
State string
}

func buildAIContext(con *console.SliverClient) aiContext {
ctx := aiContext{
target: aiTargetSummary{
Label: "No active target",
Host: "Select a session or beacon with `use`",
Mode: "offline preview",
C2: "n/a",
OS: "unknown",
Arch: "unknown",
},
connection: aiConnectionSummary{
Profile: "<disconnected>",
Server: "<unknown>",
Operator: "<unknown>",
State: "idle",
},
status: "Loading AI conversations from the server...",
}

if con != nil {
if details, state, ok := con.CurrentConnection(); ok {
ctx.connection.State = strings.ToLower(state.String())
if details != nil && details.Config != nil {
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
}
}

session, beacon := con.ActiveTarget.Get()
switch {
case session != nil:
ctx.target = aiTargetSummary{
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
Host: fallback(session.Hostname, "<unknown host>"),
OS: fallback(session.OS, "unknown"),
Arch: fallback(session.Arch, "unknown"),
C2: fallback(session.ActiveC2, "unknown"),
Mode: "interactive session",
Details: []string{
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
fmt.Sprintf("PID: %d", session.PID),
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
},
}
case beacon != nil:
ctx.target = aiTargetSummary{
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
Host: fallback(beacon.Hostname, "<unknown host>"),
OS: fallback(beacon.OS, "unknown"),
Arch: fallback(beacon.Arch, "unknown"),
C2: fallback(beacon.ActiveC2, "unknown"),
Mode: "asynchronous beacon",
Details: []string{
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
},
}
}
}

return ctx
}

func fallback(value, def string) string {
value = strings.TrimSpace(value)
if value == "" {
return def
}
return value
}

func formatUnix(ts int64) string {
if ts <= 0 {
return "<unknown>"
}
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
}
31 changes: 31 additions & 0 deletions client/command/ai/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai

import (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)

// Commands returns the ai command.
func Commands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
}

// ServerCommands returns the ai command for the top-level client REPL.
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
}

func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
return &cobra.Command{
Use: consts.AIStr,
Short: "Open the Sliver AI conversation TUI",
Long: help.GetHelpFor([]string{consts.AIStr}),
Args: cobra.NoArgs,
GroupID: groupID,
Run: func(cmd *cobra.Command, args []string) {
AICmd(cmd, con, args)
},
}
}
Loading
Loading