mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(peers): ceilings, error identities and the dispatch tool's own words follow the switch being the consent
This commit is contained in:
1 parent
ddba148a36
commit
2002ef72ea
10 files changed
+101
-12
No files matched your search
+3
-1
@@ -144,7 +144,9 @@ allow = ["Bash(go test:*)"] # never prompted
|
||||
|
||||
[peers]
|
||||
# enabled = false # your sessions on this machine can message each other; every message waits for that session's user to approve it; also a switch in Settings, applied at the next turn (user config only)
|
||||
# dispatch = false # the model may hand a task to a session in another of your projects, via dispatch_to_project; needs enabled (user config only)
|
||||
# dispatch = false # the model may hand a task to a session in another of your projects, via dispatch_to_project; needs enabled; turning it on is your consent for those sessions' tasks and replies, no card (user config only)
|
||||
# max_dispatch_depth = 1 # how many sessions deep a dispatch may chain; 0 removes the ceiling (user config only)
|
||||
# max_dispatched_sessions = 8 # sessions one session may keep open from its dispatches; 0 removes the ceiling (user config only)
|
||||
# release_projects = [] # projects that take messages from your other sessions without the card; their own approvals and sandbox still apply (user config only)
|
||||
|
||||
[serve]
|
||||
|
||||
+3
-1
@@ -153,7 +153,9 @@ allow = ["Bash(go test:*)"] # 从不询问
|
||||
|
||||
[peers]
|
||||
# enabled = false # 本机你的各个会话可以互发消息;每条消息都要对方会话的用户批准;设置里也有开关,下个回合起生效(只认用户级配置)
|
||||
# dispatch = false # 模型可用 dispatch_to_project 把任务交给你另一个项目里的会话;需要 enabled(只认用户级配置)
|
||||
# dispatch = false # 模型可用 dispatch_to_project 把任务交给你另一个项目里的会话;需要 enabled;打开就是你对这些会话的任务和回信的总同意,不再出审批卡(只认用户级配置)
|
||||
# max_dispatch_depth = 1 # 派发最多能连着开几层会话;0 表示不限(只认用户级配置)
|
||||
# max_dispatched_sessions = 8 # 一个会话通过派发最多同时开着几个会话;0 表示不限(只认用户级配置)
|
||||
# release_projects = [] # 这些项目收到你其它会话的消息时不弹批准卡;它们自己的审批与沙盒照旧(只认用户级配置)
|
||||
|
||||
[serve]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/state/peermail"
|
||||
)
|
||||
|
||||
func (p *peerToolProvider) sawInMessages(needle string) bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
for _, req := range p.reqs {
|
||||
for _, m := range req.Messages {
|
||||
if strings.Contains(m.Content, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// While the user's dispatch switch is on, a task from the session that opened
|
||||
// this one reaches the model with no card, framed as data from that session and
|
||||
// not as something the user read; the same words from a session the host never
|
||||
// paired it with, or signed with another secret, wait for the user as before.
|
||||
func TestEffectADispatchedTaskReachesTheModelWithoutACardAndAStrangersDoesNot(t *testing.T) {
|
||||
const parent = "s-00000000000000bb"
|
||||
secret := []byte("0123456789abcdef0123456789abcdef")
|
||||
rec, ctrl, peersDir, seen := buildPeerToolSession(t, true, "nobody", "hello", "dispatch = true\n")
|
||||
t.Setenv("REASONIX_HOME", filepath.Dir(peersDir))
|
||||
receiver := ctrl.PeerKey()
|
||||
if receiver == "" {
|
||||
t.Fatal("the receiving session did not publish itself")
|
||||
}
|
||||
ctrl.MarkOpenedByDispatch(parent, secret, 1)
|
||||
d, err := peermail.Open(peersDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
send := func(id, body string, signWith []byte) {
|
||||
t.Helper()
|
||||
m := peermail.Message{ID: id, SentAt: time.Now().UTC(), Body: body,
|
||||
From: peermail.Party{SessionID: parent, Title: "planner", Project: "alpha"}, To: peermail.Party{SessionID: receiver}}
|
||||
m.Proof = peermail.Proof(signWith, m)
|
||||
if err := d.Send(m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
send("m-forged", "FORGED-TASK-WITH-ANOTHER-SECRET", []byte("another secret another secret!!!"))
|
||||
ctrl.RefreshPeers()
|
||||
if held := ctrl.HeldPeerMessages(); len(held) != 1 || held[0].Body != "FORGED-TASK-WITH-ANOTHER-SECRET" {
|
||||
t.Fatalf("held = %+v, want the badly signed task waiting for a card", held)
|
||||
}
|
||||
|
||||
send("m-real", "ROTATE-THE-KEY-TASK", secret)
|
||||
ctrl.RefreshPeers()
|
||||
waitFor(t, "the dispatched task to reach the provider", func() bool { return rec.sawInMessages("ROTATE-THE-KEY-TASK") })
|
||||
if rec.sawInMessages("FORGED-TASK-WITH-ANOTHER-SECRET") {
|
||||
t.Fatal("a task signed with another secret reached the model")
|
||||
}
|
||||
if !rec.sawInMessages("has not read this message") || !rec.sawInMessages("not an instruction from the user") {
|
||||
t.Fatal("the dispatched task reached the model without saying the user did not read it and that it is data")
|
||||
}
|
||||
// The stub provider's own send_peer_message call is the only card there may be:
|
||||
// nothing asked the user to let the dispatched task in.
|
||||
seen.mu.Lock()
|
||||
for _, card := range seen.approvals {
|
||||
if card.Tool != "send_peer_message" {
|
||||
t.Errorf("unexpected approval card: %+v", card)
|
||||
}
|
||||
}
|
||||
seen.mu.Unlock()
|
||||
if got := ctrl.HeldPeerMessages(); len(got) != 1 {
|
||||
t.Fatalf("held = %+v, want only the badly signed one still waiting", got)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (h *Hub) dispatchTo(ctx context.Context, from *Runtime, project, text strin
|
||||
}
|
||||
sender, ok := from.Server.Controller().(*control.Controller)
|
||||
if !ok {
|
||||
return control.DispatchResult{}, control.ErrDispatchUnavailable
|
||||
return control.DispatchResult{}, control.ErrDispatchNotLocal
|
||||
}
|
||||
target := h.runtimeByID(h.dispatch.get(from.ID, root))
|
||||
var secret []byte
|
||||
@@ -84,7 +84,7 @@ func (h *Hub) dispatchTo(ctx context.Context, from *Runtime, project, text strin
|
||||
}
|
||||
targetCtrl, ok := target.Server.Controller().(*control.Controller)
|
||||
if !ok {
|
||||
return control.DispatchResult{}, control.ErrDispatchUnavailable
|
||||
return control.DispatchResult{}, control.ErrDispatchNotLocal
|
||||
}
|
||||
key := targetCtrl.PeerKey()
|
||||
if key == "" {
|
||||
@@ -122,7 +122,7 @@ func (h *Hub) openDispatchSession(ctx context.Context, from *Runtime, sender *co
|
||||
ctrl, ok := target.Server.Controller().(*control.Controller)
|
||||
if !ok {
|
||||
_ = h.Close(target.ID)
|
||||
return nil, control.ErrDispatchUnavailable
|
||||
return nil, control.ErrDispatchNotLocal
|
||||
}
|
||||
if ctrl.WorkspaceTrust() != config.WorkspaceTrusted {
|
||||
_ = h.Close(target.ID)
|
||||
|
||||
@@ -14,10 +14,11 @@ import (
|
||||
// Each is an identity the dispatch tool reports; none is matched by its words.
|
||||
var (
|
||||
ErrDispatchDisabled = errors.New(`the user has not turned on "Let the model hand a task to another session" (Settings > Tools > Messages between sessions, which also needs "enabled"; [peers] dispatch in the user config)`)
|
||||
ErrDispatchUnavailable = errors.New("this session cannot open sessions in other projects")
|
||||
ErrDispatchUnavailable = errors.New("this session has no window to open another session in (it is a terminal or protocol session): open the project in Reasonix Studio or the web UI to dispatch from it")
|
||||
ErrDispatchNotLocal = errors.New("this pane is not one of this machine's own sessions (it is driven over SSH or by another window), so it cannot open a session here")
|
||||
ErrDispatchUnknownProject = errors.New("no project of yours answers to that name")
|
||||
ErrDispatchAmbiguous = errors.New("more than one project answers to that name")
|
||||
ErrDispatchNested = errors.New("a session opened by a dispatch cannot dispatch further")
|
||||
ErrDispatchNested = errors.New("this session is as many dispatches deep as your ceiling allows (peers.max_dispatch_depth in the user config; 0 removes it)")
|
||||
ErrDispatchUntrusted = errors.New("that project is not one you have trusted")
|
||||
ErrDispatchFailed = errors.New("could not open a session in that project")
|
||||
ErrDispatchLimit = errors.New("this session already has as many dispatched sessions open as your ceiling allows (peers.max_dispatched_sessions)")
|
||||
|
||||
@@ -3,6 +3,7 @@ package control
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -127,6 +128,7 @@ func (c *Controller) peerSettingsText() string {
|
||||
if s.Pending {
|
||||
b.WriteString(" (applies when the next turn starts)")
|
||||
}
|
||||
fmt.Fprintf(&b, "\ndispatch ceilings: depth %s, open sessions %s (0 removes a ceiling)", strconv.Itoa(s.MaxDispatchDepth), strconv.Itoa(s.MaxDispatchedSessions))
|
||||
for _, r := range s.ReleaseProjects {
|
||||
fmt.Fprintf(&b, "\nreleased project: %s", r.Path)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func peerErrorCode(err error) string {
|
||||
"peer.disabled": ErrPeersDisabled, "peer.not_found": ErrPeerNotFound, "peer.ambiguous": ErrPeerAmbiguous,
|
||||
"peer.self": ErrPeerSelf, "peer.refused": ErrPeerRefused, "peer.rate_limited": ErrPeerRateLimited,
|
||||
"peer.empty": ErrPeerEmpty, "peer.too_large": ErrPeerTooLarge,
|
||||
"dispatch.disabled": ErrDispatchDisabled, "dispatch.unavailable": ErrDispatchUnavailable, "dispatch.unknown_project": ErrDispatchUnknownProject,
|
||||
"dispatch.disabled": ErrDispatchDisabled, "dispatch.unavailable": ErrDispatchUnavailable, "dispatch.not_local": ErrDispatchNotLocal, "dispatch.unknown_project": ErrDispatchUnknownProject,
|
||||
"dispatch.ambiguous": ErrDispatchAmbiguous, "dispatch.nested": ErrDispatchNested,
|
||||
"dispatch.untrusted": ErrDispatchUntrusted, "dispatch.failed": ErrDispatchFailed, "dispatch.limit": ErrDispatchLimit,
|
||||
} {
|
||||
|
||||
@@ -18,7 +18,7 @@ type dispatchToProject struct{ link *tool.PeerLink }
|
||||
|
||||
func (*dispatchToProject) Name() string { return "dispatch_to_project" }
|
||||
func (*dispatchToProject) Description() string {
|
||||
return "Hand a task to a session in another of the user's projects: name the project (its folder name or full path, as the user has it in their project list) and write what that session should do. A session is opened there if none of yours is already waiting. The message is held until that project's own user approves it (unless they released that project); only then does its model read it, labelled as coming from you, and it works under its own permissions, not yours. It is for passing a task and its context, not a way to write into that project yourself. You get the receiving session's key; it can answer you with send_peer_message. Do not include secrets."
|
||||
return "Hand a task to a session in another of the user's projects: name the project (its folder name or full path, as the user has it in their project list) and write what that session should do. A session is opened there if none of yours is already waiting. The user's dispatch switch is their consent: the receiving session's model reads the task at once, labelled as coming from you, and it works under permissions no wider than yours. A project the user has not trusted is refused. It is for passing a task and its context, not a way to write into that project yourself. You get the receiving session's key; it can answer you with send_peer_message. Do not include secrets."
|
||||
}
|
||||
func (*dispatchToProject) Schema() json.RawMessage {
|
||||
return json.RawMessage(`{"type":"object","properties":{"project":{"type":"string","description":"The project's folder name or full path, as it appears in the user's project list"},"text":{"type":"string","description":"The task and the context the other session needs"}},"required":["project","text"],"additionalProperties":false}`)
|
||||
@@ -42,11 +42,11 @@ func (t *dispatchToProject) Execute(ctx context.Context, args json.RawMessage) (
|
||||
}
|
||||
d, ok := dir.(tool.ProjectDispatcher)
|
||||
if !ok {
|
||||
return "", errors.New("dispatch.unavailable: this session cannot open sessions in other projects")
|
||||
return "", errors.New("dispatch.unavailable: this session has no window to open another session in; open the project in Reasonix Studio or the web UI to dispatch from it")
|
||||
}
|
||||
r, err := d.DispatchToProject(ctx, in.Project, in.Text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Handed to a session in %q as %s (key %s). It is read only after that project's user approves it; it can answer with send_peer_message to your key.", r.Project, r.ID, r.Key), nil
|
||||
return fmt.Sprintf("Handed to a session in %q as %s (key %s). It can answer you with send_peer_message to your key; its reply arrives labelled as data from that session, not as an instruction.", r.Project, r.ID, r.Key), nil
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestDispatchToolHandsTheTaskOverAndReportsWhereItWent(t *testing.T) {
|
||||
if err != nil || d.asked != [2]string{"billing", "rotate the key"} {
|
||||
t.Fatalf("out %q err %v asked %v", out, err, d.asked)
|
||||
}
|
||||
for _, want := range []string{"/work/billing", "m-7", "s-9", "approves"} {
|
||||
for _, want := range []string{"/work/billing", "m-7", "s-9", "not as an instruction"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("result lacks %q: %s", want, out)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- 变更:打开设置里的“派发”开关就是对派发的总同意:任务和回信不再出审批卡;新会话沿用发起会话的模型、预设、审批档位和计划模式,且不会更宽。见 #13526。
|
||||
- Changed: the Dispatch switch is your consent: tasks and replies come in with no card, and the opened session keeps the sender's model, preset, approval mode and planning, never wider. See #13526.
|
||||
Reference in new issue
Block a user