fix(commands): enforce reject-vs-passthrough command policy
This commit is contained in:
parent
bb4a0dc766
commit
33ba65fc91
3 changed files with 153 additions and 51 deletions
|
|
@ -75,29 +75,7 @@ func builtinDefinitions(cfg *config.Config, runtime Runtime) []Definition {
|
||||||
Usage: "/show [model|channel]",
|
Usage: "/show [model|channel]",
|
||||||
Channels: []string{"telegram"},
|
Channels: []string{"telegram"},
|
||||||
Handler: func(_ context.Context, req Request) error {
|
Handler: func(_ context.Context, req Request) error {
|
||||||
if req.Reply == nil {
|
return handleShowCommand(req, cfg)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
return req.Reply("Command unavailable in current context.")
|
|
||||||
}
|
|
||||||
args := commandArgs(req.Text)
|
|
||||||
if args == "" {
|
|
||||||
return req.Reply("Usage: /show [model|channel]")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args {
|
|
||||||
case "model":
|
|
||||||
return req.Reply(fmt.Sprintf(
|
|
||||||
"Current Model: %s (Provider: %s)",
|
|
||||||
cfg.Agents.Defaults.GetModelName(),
|
|
||||||
cfg.Agents.Defaults.Provider,
|
|
||||||
))
|
|
||||||
case "channel":
|
|
||||||
return req.Reply(fmt.Sprintf("Current Channel: %s", req.Channel))
|
|
||||||
default:
|
|
||||||
return req.Reply(fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -106,34 +84,7 @@ func builtinDefinitions(cfg *config.Config, runtime Runtime) []Definition {
|
||||||
Usage: "/list [models|channels]",
|
Usage: "/list [models|channels]",
|
||||||
Channels: []string{"telegram"},
|
Channels: []string{"telegram"},
|
||||||
Handler: func(_ context.Context, req Request) error {
|
Handler: func(_ context.Context, req Request) error {
|
||||||
if req.Reply == nil {
|
return handleListCommand(req, cfg)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
return req.Reply("Command unavailable in current context.")
|
|
||||||
}
|
|
||||||
args := commandArgs(req.Text)
|
|
||||||
if args == "" {
|
|
||||||
return req.Reply("Usage: /list [models|channels]")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args {
|
|
||||||
case "models":
|
|
||||||
provider := cfg.Agents.Defaults.Provider
|
|
||||||
if provider == "" {
|
|
||||||
provider = "configured default"
|
|
||||||
}
|
|
||||||
return req.Reply(fmt.Sprintf(
|
|
||||||
"Configured Model: %s\nProvider: %s\n\nTo change models, update config.json",
|
|
||||||
cfg.Agents.Defaults.GetModelName(),
|
|
||||||
provider,
|
|
||||||
))
|
|
||||||
case "channels":
|
|
||||||
enabled := enabledChannels(cfg)
|
|
||||||
return req.Reply(fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")))
|
|
||||||
default:
|
|
||||||
return req.Reply(fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -262,6 +213,59 @@ func handleSessionCommand(req Request, runtime Runtime) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleShowCommand(req Request, cfg *config.Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return reply(req, "Command unavailable in current context.")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := commandArgs(req.Text)
|
||||||
|
if args == "" {
|
||||||
|
return reply(req, "Usage: /show [model|channel]")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args {
|
||||||
|
case "model":
|
||||||
|
return reply(req, fmt.Sprintf(
|
||||||
|
"Current Model: %s (Provider: %s)",
|
||||||
|
cfg.Agents.Defaults.GetModelName(),
|
||||||
|
cfg.Agents.Defaults.Provider,
|
||||||
|
))
|
||||||
|
case "channel":
|
||||||
|
return reply(req, fmt.Sprintf("Current Channel: %s", req.Channel))
|
||||||
|
default:
|
||||||
|
return reply(req, fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleListCommand(req Request, cfg *config.Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return reply(req, "Command unavailable in current context.")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := commandArgs(req.Text)
|
||||||
|
if args == "" {
|
||||||
|
return reply(req, "Usage: /list [models|channels]")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args {
|
||||||
|
case "models":
|
||||||
|
provider := cfg.Agents.Defaults.Provider
|
||||||
|
if provider == "" {
|
||||||
|
provider = "configured default"
|
||||||
|
}
|
||||||
|
return reply(req, fmt.Sprintf(
|
||||||
|
"Configured Model: %s\nProvider: %s\n\nTo change models, update config.json",
|
||||||
|
cfg.Agents.Defaults.GetModelName(),
|
||||||
|
provider,
|
||||||
|
))
|
||||||
|
case "channels":
|
||||||
|
enabled := enabledChannels(cfg)
|
||||||
|
return reply(req, fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")))
|
||||||
|
default:
|
||||||
|
return reply(req, fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reply(req Request, text string) error {
|
func reply(req Request, text string) error {
|
||||||
if req.Reply == nil {
|
if req.Reply == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -101,3 +101,25 @@ func TestBuiltinDefinitionsWithRuntime_EnablesSessionHandlers(t *testing.T) {
|
||||||
t.Fatalf("/session should provide runtime-backed handler when runtime is available")
|
t.Fatalf("/session should provide runtime-backed handler when runtime is available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuiltinDefinitions_ShowAndListAreTelegramOnlyHandlers(t *testing.T) {
|
||||||
|
defs := BuiltinDefinitions(&config.Config{})
|
||||||
|
|
||||||
|
defByName := map[string]Definition{}
|
||||||
|
for _, def := range defs {
|
||||||
|
defByName[def.Name] = def
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{"show", "list"} {
|
||||||
|
def, ok := defByName[name]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing /%s definition", name)
|
||||||
|
}
|
||||||
|
if def.Handler == nil {
|
||||||
|
t.Fatalf("/%s should provide a builtin handler", name)
|
||||||
|
}
|
||||||
|
if len(def.Channels) != 1 || def.Channels[0] != "telegram" {
|
||||||
|
t.Fatalf("/%s channels=%v, want [telegram]", name, def.Channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
76
pkg/commands/show_list_handlers_test.go
Normal file
76
pkg/commands/show_list_handlers_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShowListHandlers_ChannelPolicy(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
ex := NewExecutor(NewRegistry(BuiltinDefinitions(cfg)))
|
||||||
|
|
||||||
|
var telegramReply string
|
||||||
|
handled := ex.Execute(context.Background(), Request{
|
||||||
|
Channel: "telegram",
|
||||||
|
Text: "/show channel",
|
||||||
|
Reply: func(text string) error {
|
||||||
|
telegramReply = text
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if handled.Outcome != OutcomeHandled {
|
||||||
|
t.Fatalf("telegram /show outcome=%v, want=%v", handled.Outcome, OutcomeHandled)
|
||||||
|
}
|
||||||
|
if telegramReply != "Current Channel: telegram" {
|
||||||
|
t.Fatalf("telegram /show reply=%q, want=%q", telegramReply, "Current Channel: telegram")
|
||||||
|
}
|
||||||
|
|
||||||
|
rejected := ex.Execute(context.Background(), Request{
|
||||||
|
Channel: "whatsapp",
|
||||||
|
Text: "/show channel",
|
||||||
|
})
|
||||||
|
if rejected.Outcome != OutcomeRejected {
|
||||||
|
t.Fatalf("whatsapp /show outcome=%v, want=%v", rejected.Outcome, OutcomeRejected)
|
||||||
|
}
|
||||||
|
if rejected.Command != "show" {
|
||||||
|
t.Fatalf("whatsapp /show command=%q, want=%q", rejected.Command, "show")
|
||||||
|
}
|
||||||
|
if rejected.Reply != "Command /show is not supported on whatsapp." {
|
||||||
|
t.Fatalf("whatsapp /show reply=%q, want=%q", rejected.Reply, "Command /show is not supported on whatsapp.")
|
||||||
|
}
|
||||||
|
|
||||||
|
passthrough := ex.Execute(context.Background(), Request{
|
||||||
|
Channel: "whatsapp",
|
||||||
|
Text: "/foo",
|
||||||
|
})
|
||||||
|
if passthrough.Outcome != OutcomePassthrough {
|
||||||
|
t.Fatalf("whatsapp /foo outcome=%v, want=%v", passthrough.Outcome, OutcomePassthrough)
|
||||||
|
}
|
||||||
|
if passthrough.Command != "foo" {
|
||||||
|
t.Fatalf("whatsapp /foo command=%q, want=%q", passthrough.Command, "foo")
|
||||||
|
}
|
||||||
|
if passthrough.Reply != "" {
|
||||||
|
t.Fatalf("whatsapp /foo reply=%q, want empty", passthrough.Reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowListHandlers_ListRejectsUnsupportedChannel(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
ex := NewExecutor(NewRegistry(BuiltinDefinitions(cfg)))
|
||||||
|
|
||||||
|
res := ex.Execute(context.Background(), Request{
|
||||||
|
Channel: "whatsapp",
|
||||||
|
Text: "/list channels",
|
||||||
|
})
|
||||||
|
if res.Outcome != OutcomeRejected {
|
||||||
|
t.Fatalf("whatsapp /list outcome=%v, want=%v", res.Outcome, OutcomeRejected)
|
||||||
|
}
|
||||||
|
if res.Command != "list" {
|
||||||
|
t.Fatalf("whatsapp /list command=%q, want=%q", res.Command, "list")
|
||||||
|
}
|
||||||
|
if res.Reply != "Command /list is not supported on whatsapp." {
|
||||||
|
t.Fatalf("whatsapp /list reply=%q, want=%q", res.Reply, "Command /list is not supported on whatsapp.")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue