mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(peers): a dispatch proof is valid once per sequence number, and the adopt/spool boundaries are pinned
This commit is contained in:
1 parent
6943f966da
commit
ba8900aba9
8 files changed
+181
-16
No files matched your search
@@ -40,9 +40,11 @@ func TestEffectADispatchedTaskReachesTheModelWithoutACardAndAStrangersDoesNot(t
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var seq uint64
|
||||
send := func(id, body string, signWith []byte) {
|
||||
t.Helper()
|
||||
m := peermail.Message{ID: id, SentAt: time.Now().UTC(), Body: body,
|
||||
seq++
|
||||
m := peermail.Message{ID: id, SentAt: time.Now().UTC(), Body: body, Seq: seq,
|
||||
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 {
|
||||
|
||||
@@ -21,6 +21,14 @@ func TestAConfinedCommandCannotTouchThePeerSpool(t *testing.T) {
|
||||
if runtime.GOOS == "windows" || !sandbox.Available() {
|
||||
t.Skip("no OS-level bash confinement on this machine")
|
||||
}
|
||||
t.Run("workspace beside the state root", func(t *testing.T) { confinedSpoolCase(t, false) })
|
||||
// The workspace being the state root's parent (a project opened at the home
|
||||
// directory) puts the spool inside a writable root; the forbidden-root rule
|
||||
// has to win there too.
|
||||
t.Run("workspace above the state root", func(t *testing.T) { confinedSpoolCase(t, true) })
|
||||
}
|
||||
|
||||
func confinedSpoolCase(t *testing.T, workspaceAbove bool) {
|
||||
base, err := os.MkdirTemp(os.Getenv("HOME"), ".reasonix-peers-test-")
|
||||
if err != nil {
|
||||
t.Skipf("cannot create a work dir under home: %v", err)
|
||||
@@ -32,6 +40,9 @@ func TestAConfinedCommandCannotTouchThePeerSpool(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if workspaceAbove {
|
||||
workspace = base
|
||||
}
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
|
||||
@@ -44,7 +44,46 @@ type dispatchState struct {
|
||||
mu sync.Mutex
|
||||
opened bool
|
||||
depth int
|
||||
pairs map[string][]byte
|
||||
pairs map[string]*dispatchPair
|
||||
}
|
||||
|
||||
// dispatchWindow is how many accepted sequence numbers a pairing remembers; one
|
||||
// older than the newest by more than this is refused unseen.
|
||||
const dispatchWindow = 4096
|
||||
|
||||
// dispatchPair is one pairing: the secret, the next number to sign with, and the
|
||||
// numbers taken from the partner. It lives as long as the secret does, in memory
|
||||
// only, so a message copied out of the spool cannot be replayed into this pairing
|
||||
// however many others have been read since.
|
||||
type dispatchPair struct {
|
||||
secret []byte
|
||||
sent uint64
|
||||
seen map[uint64]bool
|
||||
floor uint64
|
||||
}
|
||||
|
||||
// accept records seq as taken, or refuses it when it was taken or has aged out.
|
||||
func (p *dispatchPair) accept(seq uint64) bool {
|
||||
if seq == 0 || seq <= p.floor || p.seen[seq] {
|
||||
return false
|
||||
}
|
||||
if p.seen == nil {
|
||||
p.seen = map[uint64]bool{}
|
||||
}
|
||||
p.seen[seq] = true
|
||||
if len(p.seen) > dispatchWindow {
|
||||
var newest uint64
|
||||
for n := range p.seen {
|
||||
newest = max(newest, n)
|
||||
}
|
||||
p.floor = newest - dispatchWindow
|
||||
for n := range p.seen {
|
||||
if n <= p.floor {
|
||||
delete(p.seen, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SetProjectDispatcher gives the dispatch tool somewhere to go; a controller
|
||||
@@ -94,27 +133,33 @@ func (c *Controller) rememberPairLocked(key string, secret []byte) {
|
||||
return
|
||||
}
|
||||
if c.dispatch.pairs == nil {
|
||||
c.dispatch.pairs = map[string][]byte{}
|
||||
c.dispatch.pairs = map[string]*dispatchPair{}
|
||||
}
|
||||
c.dispatch.pairs[key] = append([]byte(nil), secret...)
|
||||
c.dispatch.pairs[key] = &dispatchPair{secret: append([]byte(nil), secret...)}
|
||||
}
|
||||
|
||||
// dispatchProof signs a message to a dispatch partner, or returns "" when the
|
||||
// target is not one.
|
||||
func (c *Controller) dispatchProof(m peermail.Message) string {
|
||||
// signDispatch numbers and signs a message to a dispatch partner, and leaves it
|
||||
// alone when the target is not one.
|
||||
func (c *Controller) signDispatch(m *peermail.Message) {
|
||||
c.dispatch.mu.Lock()
|
||||
secret := c.dispatch.pairs[m.To.SessionID]
|
||||
c.dispatch.mu.Unlock()
|
||||
return peermail.Proof(secret, m)
|
||||
defer c.dispatch.mu.Unlock()
|
||||
pair := c.dispatch.pairs[m.To.SessionID]
|
||||
if pair == nil {
|
||||
return
|
||||
}
|
||||
pair.sent++
|
||||
m.Seq = pair.sent
|
||||
m.Proof = peermail.Proof(pair.secret, *m)
|
||||
}
|
||||
|
||||
// dispatchPartner reports whether the message proves it came from a session this
|
||||
// one dispatched to or was dispatched by.
|
||||
// one dispatched to or was dispatched by, and that it is the first time it is
|
||||
// taken: a proof is valid once per sequence number.
|
||||
func (c *Controller) dispatchPartner(m peermail.Message) bool {
|
||||
c.dispatch.mu.Lock()
|
||||
secret := c.dispatch.pairs[m.From.SessionID]
|
||||
c.dispatch.mu.Unlock()
|
||||
return peermail.ProofValid(secret, m)
|
||||
defer c.dispatch.mu.Unlock()
|
||||
pair := c.dispatch.pairs[m.From.SessionID]
|
||||
return pair != nil && peermail.ProofValid(pair.secret, m) && pair.accept(m.Seq)
|
||||
}
|
||||
|
||||
// PeerKey is the address other sessions write to this one at, empty until the
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/safety/permission"
|
||||
"reasonix/internal/state/peermail"
|
||||
)
|
||||
|
||||
func pairedController(t *testing.T) (*Controller, []byte) {
|
||||
t.Helper()
|
||||
f := newPeerFixture(t)
|
||||
secret := []byte("0123456789abcdef0123456789abcdef")
|
||||
f.c.NoteDispatchedSession("s-peer", secret)
|
||||
return f.c, secret
|
||||
}
|
||||
|
||||
func signedFrom(c *Controller, secret []byte, seq uint64) peermail.Message {
|
||||
m := peermail.Message{ID: "m-x", From: peermail.Party{SessionID: "s-peer"}, To: peermail.Party{SessionID: "s-me"}, SentAt: time.Now().UTC(), Body: "task", Seq: seq}
|
||||
m.Proof = peermail.Proof(secret, m)
|
||||
return m
|
||||
}
|
||||
|
||||
// A message the receiver already took in proves nothing a second time: the seen
|
||||
// log forgets ids after a thousand acknowledgements, so the pairing itself
|
||||
// remembers which sequence numbers it has accepted for as long as it lives.
|
||||
func TestAProofedMessageIsAcceptedOnceForThePairingsLifetime(t *testing.T) {
|
||||
c, secret := pairedController(t)
|
||||
first := signedFrom(c, secret, 1)
|
||||
if !c.dispatchPartner(first) {
|
||||
t.Fatal("a fresh proofed message was refused")
|
||||
}
|
||||
if c.dispatchPartner(first) {
|
||||
t.Fatal("the same proofed message was accepted twice")
|
||||
}
|
||||
if !c.dispatchPartner(signedFrom(c, secret, 2)) {
|
||||
t.Fatal("the next sequence number was refused")
|
||||
}
|
||||
if c.dispatchPartner(signedFrom(c, secret, 0)) {
|
||||
t.Fatal("an unnumbered message was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// Messages are numbered in the order they were signed, not the order they are
|
||||
// read, so a later one read first must not shut out an earlier one.
|
||||
func TestAnOlderSequenceNumberStillGetsInWhileTheWindowHoldsIt(t *testing.T) {
|
||||
c, secret := pairedController(t)
|
||||
if !c.dispatchPartner(signedFrom(c, secret, 5)) || !c.dispatchPartner(signedFrom(c, secret, 3)) {
|
||||
t.Fatal("an out-of-order but unseen number was refused")
|
||||
}
|
||||
for i := range uint64(dispatchWindow + 10) {
|
||||
c.dispatchPartner(signedFrom(c, secret, 100+i))
|
||||
}
|
||||
if c.dispatchPartner(signedFrom(c, secret, 3)) {
|
||||
t.Fatal("a number older than the window was accepted again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachMessageSentToAPartnerCarriesTheNextSequenceNumber(t *testing.T) {
|
||||
c, _ := pairedController(t)
|
||||
var got []uint64
|
||||
for range 3 {
|
||||
m := peermail.Message{ID: "m-y", From: peermail.Party{SessionID: "s-me"}, To: peermail.Party{SessionID: "s-peer"}, SentAt: time.Now().UTC(), Body: "b"}
|
||||
c.signDispatch(&m)
|
||||
got = append(got, m.Seq)
|
||||
if m.Proof == "" {
|
||||
t.Fatal("a message to a partner went out unsigned")
|
||||
}
|
||||
}
|
||||
if got[0] != 1 || got[1] != 2 || got[2] != 3 {
|
||||
t.Fatalf("sequence numbers = %v, want 1 2 3", got)
|
||||
}
|
||||
stranger := peermail.Message{To: peermail.Party{SessionID: "s-nobody"}, Body: "b"}
|
||||
c.signDispatch(&stranger)
|
||||
if stranger.Proof != "" || stranger.Seq != 0 {
|
||||
t.Fatalf("a message to a non-partner was signed: %+v", stranger)
|
||||
}
|
||||
}
|
||||
|
||||
// The grant a dispatching session passes on sets the approval posture; it must
|
||||
// not reach past the target project's own rules. A deny rule the project holds
|
||||
// keeps refusing under an adopted yolo, and a call outside it is the only thing
|
||||
// the posture lets through.
|
||||
func TestAdoptingAnApprovalGrantDoesNotLiftTheTargetProjectsDenyRules(t *testing.T) {
|
||||
deny, ok := permission.ParseRule("bash(rm:*)")
|
||||
if !ok {
|
||||
t.Fatal("ParseRule failed")
|
||||
}
|
||||
c := New(Options{Policy: permission.Policy{Deny: []permission.Rule{deny}}})
|
||||
c.AdoptSessionGrant(SessionGrant{Approval: ToolApprovalYolo})
|
||||
if c.ToolApprovalMode() != ToolApprovalYolo {
|
||||
t.Fatalf("posture = %q, want the grant's", c.ToolApprovalMode())
|
||||
}
|
||||
gate := c.newInteractiveGate()
|
||||
if v, err := permission.VerdictOf(context.Background(), gate, "bash", bashCall(t, "rm -rf build"), false); err != nil || v.Allow {
|
||||
t.Fatalf("a call the project denies under an adopted yolo: allow=%v err=%v", v.Allow, err)
|
||||
}
|
||||
if v, err := permission.VerdictOf(context.Background(), gate, "bash", bashCall(t, "ls"), false); err != nil || !v.Allow {
|
||||
t.Fatalf("a call no rule names under yolo: allow=%v err=%v", v.Allow, err)
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (c *Controller) SendPeerMessage(to, text string) (PeerReceipt, error) {
|
||||
From: peermail.Party{SessionID: self, Title: peerAttribute(sessionstore.BranchID(path)), Project: peerAttribute(baseName(c.WorkspaceRoot()))},
|
||||
To: peermail.Party{SessionID: target.SessionID, WriterID: target.WriterID, Title: target.Title, Project: target.Project},
|
||||
}
|
||||
msg.Proof = c.dispatchProof(msg)
|
||||
c.signDispatch(&msg)
|
||||
if err := dir.Send(msg); err != nil {
|
||||
return PeerReceipt{}, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func Proof(secret []byte, m Message) string {
|
||||
return ""
|
||||
}
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
for _, part := range []string{m.ID, m.From.SessionID, m.To.SessionID, strconv.FormatInt(m.SentAt.UnixNano(), 10), strconv.Itoa(m.Hops), cleanBody(m.Body)} {
|
||||
for _, part := range []string{m.ID, m.From.SessionID, m.To.SessionID, strconv.FormatInt(m.SentAt.UnixNano(), 10), strconv.Itoa(m.Hops), strconv.FormatUint(m.Seq, 10), cleanBody(m.Body)} {
|
||||
mac.Write([]byte(strconv.Itoa(len(part))))
|
||||
mac.Write([]byte{':'})
|
||||
mac.Write([]byte(part))
|
||||
|
||||
@@ -23,6 +23,7 @@ func TestAProofHoldsOnlyForTheMessageAndTheSecretItWasMadeFor(t *testing.T) {
|
||||
"id": func(m *Message) { m.ID = "m-2" },
|
||||
"time": func(m *Message) { m.SentAt = m.SentAt.Add(time.Nanosecond) },
|
||||
"hops": func(m *Message) { m.Hops++ },
|
||||
"seq": func(m *Message) { m.Seq++ },
|
||||
} {
|
||||
forged := m
|
||||
mutate(&forged)
|
||||
|
||||
@@ -53,6 +53,9 @@ type Message struct {
|
||||
Hops int `json:"hops"`
|
||||
Body string `json:"body"`
|
||||
Proof string `json:"proof,omitempty"`
|
||||
// Seq numbers the messages one session signs for one partner, from 1. The
|
||||
// proof covers it, so the receiver can refuse a number it has already taken.
|
||||
Seq uint64 `json:"seq,omitempty"`
|
||||
}
|
||||
|
||||
// Live describes one running session. It is a description: whether the session
|
||||
|
||||
Reference in new issue
Block a user