Skip to content

Commit f3f51a6

Browse files
committed
fix(agent): allow private chat session reset
1 parent c8cdc45 commit f3f51a6

2 files changed

Lines changed: 55 additions & 9 deletions

File tree

internal/agent/slash.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ func (a *Agent) handleSlashCommand(msg bus.InboundMessage) slashResult {
4141
// Owner-only gate for write commands. Read-only inspections (/status,
4242
// /usage, /insights, /help, /version, /start, /whoami) stay open so
4343
// any group member can self-serve info. Mutators that change the
44-
// agent's runtime state (model, personality) or the session history
45-
// (new/reset/undo/retry/compact) are restricted to the agent owner
46-
// + per-channel admin allowlist — without this gate, anyone in a
47-
// Discord guild could `/model haiku` and silently downgrade a shared
48-
// agent for everyone else.
49-
if writeSlashCommands[cmd] && !a.isAdminChatter(msg) {
44+
// agent's runtime state (model, personality) or shared group-session
45+
// history are restricted to the agent owner + per-channel admin
46+
// allowlist. A DM chatter may start a fresh copy of their own session
47+
// with /new or /reset; those commands don't affect anybody else there.
48+
if slashRequiresAdmin(cmd, msg) && !a.isAdminChatter(msg) {
5049
return slashResult{
5150
handled: true,
5251
reply: fmt.Sprintf("🔒 `%s` 只有 agent owner / admin 能用。让 owner 把你的 platform 用户 ID 加进 agent.json 的 `admins.%s` 里(用 `/whoami` 查自己的 ID)。", cmd, msg.Channel),
@@ -152,6 +151,19 @@ var writeSlashCommands = map[string]bool{
152151
"/personality": true,
153152
}
154153

154+
// slashRequiresAdmin keeps agent-wide mutations owner/admin-only and also
155+
// protects shared group history. Starting a fresh private session is a
156+
// per-chatter operation, so /new and /reset stay available outside groups.
157+
func slashRequiresAdmin(cmd string, msg bus.InboundMessage) bool {
158+
if !writeSlashCommands[cmd] {
159+
return false
160+
}
161+
if (cmd == "/new" || cmd == "/reset") && msg.PeerKind != "group" {
162+
return false
163+
}
164+
return true
165+
}
166+
155167
// isAdminChatter decides whether the chatter is allowed to run a write-mode
156168
// slash command on this channel.
157169
//
@@ -492,9 +504,10 @@ Info
492504
/version — Show version
493505
/whoami — Show your platform user ID
494506
495-
🔒 Write commands (/new /reset /undo /retry /compact /model /personality)
496-
in IM channels are restricted to the agent owner + admins listed in
497-
agent.json's "admins" field. Use /whoami to find your ID.`
507+
🔒 Agent-wide write commands (/undo /retry /compact /model /personality)
508+
and group-chat /new or /reset are restricted to the agent owner + admins
509+
listed in agent.json's "admins" field. Private-chat /new and /reset are
510+
available to the chatter. Use /whoami to find your ID.`
498511
}
499512

500513
// slashPlan handles `/plan <task>`: republish the rest of the message

internal/agent/slash_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package agent
2+
3+
import (
4+
"testing"
5+
6+
"github.com/fastclaw-ai/fastclaw/internal/bus"
7+
)
8+
9+
func TestSlashRequiresAdmin(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
cmd string
13+
peerKind string
14+
want bool
15+
}{
16+
{name: "new in dm", cmd: "/new", peerKind: "dm", want: false},
17+
{name: "reset in dm", cmd: "/reset", peerKind: "dm", want: false},
18+
{name: "new with legacy empty peer kind", cmd: "/new", want: false},
19+
{name: "new in group", cmd: "/new", peerKind: "group", want: true},
20+
{name: "reset in group", cmd: "/reset", peerKind: "group", want: true},
21+
{name: "model in dm", cmd: "/model", peerKind: "dm", want: true},
22+
{name: "read command", cmd: "/status", peerKind: "group", want: false},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
msg := bus.InboundMessage{PeerKind: tt.peerKind}
28+
if got := slashRequiresAdmin(tt.cmd, msg); got != tt.want {
29+
t.Fatalf("slashRequiresAdmin(%q, peer=%q) = %v, want %v", tt.cmd, tt.peerKind, got, tt.want)
30+
}
31+
})
32+
}
33+
}

0 commit comments

Comments
 (0)