Why: Independent review found scratch runs still called the project context resolver, and chat intent routing plus system.intent re-dispatch could deliver rdm.* commands with a scratch host project. Evidence: go test ./internal/runmgr ./internal/cmdbus ./internal/app ./internal/server ./internal/store pass locally (SELF); CI is authoritative. Impact: ProjectContext returns nothing when project context is disabled; Bus.Dispatch applies a host-project guard to every request carrying a host project, rejecting rdm.* and formal project commands on scratch hosts; scratch lookups that fail keep isolation gates closed. Refs: Agent-System-Backend#241; agent-exec receipt u2c-run (pi, success) Co-authored-by: Cursor <cursoragent@cursor.com>
95 lines
3.9 KiB
Go
95 lines
3.9 KiB
Go
package cmdbus
|
|
|
|
// 基座项目门禁在命令总线上的行为(临时会话隔离,U2):SetHostProjectGuard 装
|
|
// 的门禁对所有带 HostProjectID 的分发生效,且在 handler 与项目工作预检之前。
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentcommand "agent-command"
|
|
)
|
|
|
|
// TestHostProjectGuardRefusesBeforeHandler 基座项目门禁:带宿主的请求先过门禁,
|
|
// 拒绝时 handler 与预检都不该被调用;同一个命令换一个普通宿主项目照旧执行。
|
|
func TestHostProjectGuardRefusesBeforeHandler(t *testing.T) {
|
|
audit := &memAudit{}
|
|
bus := New(audit, nil, func() time.Time { return time.Unix(1700000000, 0).UTC() })
|
|
handled, preflights := 0, 0
|
|
bus.SetProjectWorkPreflight(func(context.Context, Request) error { preflights++; return nil })
|
|
if err := bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "rdm.reqs.add", Title: "登记需求"}, "",
|
|
func(context.Context, Request) (any, error) { handled++; return map[string]any{"ok": true}, nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bus.SetHostProjectGuard(func(_ context.Context, req Request) error {
|
|
if strings.HasPrefix(req.ID, "rdm.") || RequiresProjectWorkDecision(req.ID) {
|
|
if req.HostProjectID == "prj_scratch" {
|
|
return errors.New("not available in scratch sessions")
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
resp := bus.Dispatch(context.Background(), Request{
|
|
ID: "rdm.reqs.add", Entrance: "chat", Actor: "alice", Owner: "alice",
|
|
ConversationID: "cnv_scratch", HostProjectID: "prj_scratch",
|
|
})
|
|
if resp.Receipt.State != agentcommand.StateRejected {
|
|
t.Fatalf("scratch host dispatch state = %s, want rejected", resp.Receipt.State)
|
|
}
|
|
if !strings.HasPrefix(resp.Receipt.Reason, "host project: ") ||
|
|
!strings.Contains(resp.Receipt.Reason, "not available in scratch sessions") {
|
|
t.Fatalf("scratch host rejection reason = %q", resp.Receipt.Reason)
|
|
}
|
|
if handled != 0 || preflights != 0 {
|
|
t.Fatalf("guard ran late: handler=%d preflight=%d", handled, preflights)
|
|
}
|
|
|
|
// 正面:同一命令在普通项目宿主上不被这条门禁拒绝。
|
|
ok := bus.Dispatch(context.Background(), Request{
|
|
ID: "rdm.reqs.add", Entrance: "chat", Actor: "alice", Owner: "alice",
|
|
ConversationID: "cnv_normal", HostProjectID: "prj_normal",
|
|
})
|
|
if ok.Receipt.State != agentcommand.StateCommitted || handled != 1 || preflights != 1 {
|
|
t.Fatalf("normal host dispatch = %+v handler=%d preflight=%d", ok.Receipt, handled, preflights)
|
|
}
|
|
}
|
|
|
|
// TestHostProjectGuardSkipsHostlessAndUnrelatedCommands 门禁只作用于带宿主的
|
|
// 请求与非项目命令;会话类命令在同一个临时会话宿主上照旧可用。
|
|
func TestHostProjectGuardSkipsHostlessAndUnrelatedCommands(t *testing.T) {
|
|
bus := New(&memAudit{}, nil, func() time.Time { return time.Unix(1700000000, 0).UTC() })
|
|
if err := bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "demo.ping", Title: "Ping"}, "",
|
|
func(context.Context, Request) (any, error) { return map[string]any{"pong": true}, nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
guarded := 0
|
|
bus.SetHostProjectGuard(func(_ context.Context, req Request) error {
|
|
guarded++
|
|
if req.HostProjectID == "prj_scratch" && strings.HasPrefix(req.ID, "rdm.") {
|
|
return errors.New("not available in scratch sessions")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
resp := bus.Dispatch(context.Background(), Request{
|
|
ID: "demo.ping", Entrance: "chat", Actor: "alice", Owner: "alice", HostProjectID: "prj_scratch",
|
|
})
|
|
if resp.Receipt.State != agentcommand.StateCommitted {
|
|
t.Fatalf("conversation command on a scratch host = %+v", resp.Receipt)
|
|
}
|
|
if guarded != 1 {
|
|
t.Fatalf("host guard calls = %d, want 1", guarded)
|
|
}
|
|
hostless := bus.Dispatch(context.Background(), Request{ID: "demo.ping", Entrance: "api", Actor: "alice", Owner: "alice"})
|
|
if hostless.Receipt.State != agentcommand.StateCommitted {
|
|
t.Fatalf("hostless dispatch = %+v", hostless.Receipt)
|
|
}
|
|
if guarded != 1 {
|
|
t.Fatalf("host guard ran for a hostless dispatch: %d", guarded)
|
|
}
|
|
}
|