Skip to content

Commit 9cc94e5

Browse files
Copilotdgageot
andcommitted
Add enable_bang_commands configuration option
- Add EnableBangCommands field to AgentConfig (v2) - Add enableBangCommands field to Agent struct - Add WithEnableBangCommands option function - Wire configuration through teamloader to agents - Add Runtime interface method CurrentAgentEnableBangCommands() - Implement for both LocalRuntime and RemoteRuntime - Update TUI chat page to check permission before executing ! commands - Add tests for configuration parsing and agent options - Add example configurations demonstrating the feature Co-authored-by: dgageot <153495+dgageot@users.noreply.github.com>
1 parent 6092b0d commit 9cc94e5

12 files changed

Lines changed: 172 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env cagent run
2+
version: "2"
3+
4+
metadata:
5+
readme: |
6+
This is a demonstration of the enable_bang_commands configuration option.
7+
8+
This agent has enable_bang_commands set to false (or omitted, which is the default).
9+
When users try to use ! commands, they will see an error message explaining that
10+
bang commands are not enabled.
11+
12+
This is the recommended configuration for most agents, as it prevents users from
13+
accidentally executing shell commands they didn't intend to run.
14+
15+
agents:
16+
root:
17+
model: openai/gpt-4o
18+
description: "Example agent with bang commands disabled (default)"
19+
instruction: |
20+
You are a helpful assistant focused on providing information and assistance.
21+
enable_bang_commands: false
22+
toolsets:
23+
- type: think
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env cagent run
2+
version: "2"
3+
4+
metadata:
5+
readme: |
6+
This is a demonstration of the enable_bang_commands configuration option.
7+
Bang commands (!) allow users to execute shell commands directly from the TUI
8+
without going through the agent.
9+
10+
This agent has enable_bang_commands: true, so you can use commands like:
11+
- !ls
12+
- !pwd
13+
- !echo "Hello World"
14+
15+
agents:
16+
root:
17+
model: openai/gpt-4o
18+
description: "Example agent with bang commands enabled"
19+
instruction: |
20+
You are a helpful assistant. Users can also execute shell commands directly
21+
using the ! prefix (e.g., !ls, !pwd). This is handled separately from your
22+
agent capabilities.
23+
enable_bang_commands: true
24+
toolsets:
25+
- type: think

pkg/agent/agent.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Agent struct {
2626
addPromptFiles []string
2727
tools []tools.Tool
2828
commands map[string]string
29+
enableBangCommands bool
2930
pendingWarnings []string
3031
}
3132

@@ -102,6 +103,11 @@ func (a *Agent) Commands() map[string]string {
102103
return a.commands
103104
}
104105

106+
// EnableBangCommands returns whether bang commands (!) are enabled for this agent
107+
func (a *Agent) EnableBangCommands() bool {
108+
return a.enableBangCommands
109+
}
110+
105111
// Tools returns the tools available to this agent
106112
func (a *Agent) Tools(ctx context.Context) ([]tools.Tool, error) {
107113
a.ensureToolSetsAreStarted(ctx)

pkg/agent/bang_commands_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package agent
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestEnableBangCommands(t *testing.T) {
10+
t.Run("default is false", func(t *testing.T) {
11+
a := New("test", "test prompt")
12+
assert.False(t, a.EnableBangCommands())
13+
})
14+
15+
t.Run("can enable bang commands", func(t *testing.T) {
16+
a := New("test", "test prompt", WithEnableBangCommands(true))
17+
assert.True(t, a.EnableBangCommands())
18+
})
19+
20+
t.Run("can explicitly disable bang commands", func(t *testing.T) {
21+
a := New("test", "test prompt", WithEnableBangCommands(false))
22+
assert.False(t, a.EnableBangCommands())
23+
})
24+
}

pkg/agent/opts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func WithCommands(commands map[string]string) Opt {
9797
}
9898
}
9999

100+
func WithEnableBangCommands(enable bool) Opt {
101+
return func(a *Agent) {
102+
a.enableBangCommands = enable
103+
}
104+
}
105+
100106
func WithLoadTimeWarnings(warnings []string) Opt {
101107
return func(a *Agent) {
102108
for _, w := range warnings {

pkg/app/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ func (a *App) ResolveCommand(ctx context.Context, userInput string) string {
4646
return runtime.ResolveCommand(ctx, a.runtime, userInput)
4747
}
4848

49+
// IsBangCommandsEnabled returns whether bang commands are enabled for the current agent
50+
func (a *App) IsBangCommandsEnabled() bool {
51+
return a.runtime.CurrentAgentEnableBangCommands()
52+
}
53+
4954
// Run one agent loop
5055
func (a *App) Run(ctx context.Context, cancel context.CancelFunc, message string) {
5156
a.cancel = cancel
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package v2
2+
3+
import (
4+
"testing"
5+
6+
"github.com/goccy/go-yaml"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestEnableBangCommandsParsing(t *testing.T) {
12+
t.Run("parse enable_bang_commands true", func(t *testing.T) {
13+
yamlConfig := `
14+
version: "2"
15+
agents:
16+
root:
17+
model: openai/gpt-4o
18+
enable_bang_commands: true
19+
`
20+
var cfg Config
21+
err := yaml.Unmarshal([]byte(yamlConfig), &cfg)
22+
require.NoError(t, err)
23+
assert.True(t, cfg.Agents["root"].EnableBangCommands)
24+
})
25+
26+
t.Run("parse enable_bang_commands false", func(t *testing.T) {
27+
yamlConfig := `
28+
version: "2"
29+
agents:
30+
root:
31+
model: openai/gpt-4o
32+
enable_bang_commands: false
33+
`
34+
var cfg Config
35+
err := yaml.Unmarshal([]byte(yamlConfig), &cfg)
36+
require.NoError(t, err)
37+
assert.False(t, cfg.Agents["root"].EnableBangCommands)
38+
})
39+
40+
t.Run("default is false when omitted", func(t *testing.T) {
41+
yamlConfig := `
42+
version: "2"
43+
agents:
44+
root:
45+
model: openai/gpt-4o
46+
`
47+
var cfg Config
48+
err := yaml.Unmarshal([]byte(yamlConfig), &cfg)
49+
require.NoError(t, err)
50+
assert.False(t, cfg.Agents["root"].EnableBangCommands)
51+
})
52+
}

pkg/config/v2/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type AgentConfig struct {
2626
NumHistoryItems int `json:"num_history_items,omitempty"`
2727
AddPromptFiles []string `json:"add_prompt_files,omitempty" yaml:"add_prompt_files,omitempty"`
2828
Commands types.Commands `json:"commands,omitempty"`
29+
EnableBangCommands bool `json:"enable_bang_commands,omitempty"`
2930
StructuredOutput *StructuredOutput `json:"structured_output,omitempty"`
3031
}
3132

pkg/runtime/remote_runtime.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ func (r *RemoteRuntime) CurrentAgentCommands(ctx context.Context) map[string]str
7777
return map[string]string{}
7878
}
7979

80+
func (r *RemoteRuntime) CurrentAgentEnableBangCommands() bool {
81+
cfg, err := r.client.GetAgent(context.Background(), r.agentFilename)
82+
if err != nil {
83+
return false
84+
}
85+
86+
for agentName, agent := range cfg.Agents {
87+
if agentName == r.currentAgent {
88+
return agent.EnableBangCommands
89+
}
90+
}
91+
92+
return false
93+
}
94+
8095
// RunStream starts the agent's interaction loop and returns a channel of events
8196
func (r *RemoteRuntime) RunStream(ctx context.Context, sess *session.Session) <-chan Event {
8297
slog.Debug("Starting remote runtime stream", "agent", r.currentAgent, "session_id", r.sessionID)

pkg/runtime/runtime.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ type Runtime interface {
6868
CurrentAgentName() string
6969
// CurrentAgentCommands returns the commands for the active agent
7070
CurrentAgentCommands(ctx context.Context) map[string]string
71+
// CurrentAgentEnableBangCommands returns whether bang commands are enabled for the current agent
72+
CurrentAgentEnableBangCommands() bool
7173
// RunStream starts the agent's interaction loop and returns a channel of events
7274
RunStream(ctx context.Context, sess *session.Session) <-chan Event
7375
// Run starts the agent's interaction loop and returns the final messages
@@ -183,6 +185,11 @@ func (r *LocalRuntime) CurrentAgentCommands(context.Context) map[string]string {
183185
return r.CurrentAgent().Commands()
184186
}
185187

188+
// CurrentAgentEnableBangCommands returns whether bang commands are enabled for the current agent
189+
func (r *LocalRuntime) CurrentAgentEnableBangCommands() bool {
190+
return r.CurrentAgent().EnableBangCommands()
191+
}
192+
186193
// CurrentAgent returns the current agent
187194
func (r *LocalRuntime) CurrentAgent() *agent.Agent {
188195
// We validated already that the agent exists

0 commit comments

Comments
 (0)