feat: durably admit client dispatches

This commit is contained in:
2026-09-06 07:36:45 +00:00
parent f2d89d5b45
commit ac18ec49e7
2 changed files with 53 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package agent
import (
"context"
"errors"
"time"
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
"github.com/rvbox/rvbox/internal/agentproto"
"github.com/rvbox/rvbox/internal/client/spool"
"github.com/rvbox/rvbox/internal/domain"
)
// PersistDispatch admits a server dispatch to the durable spool before any
// CommandAccepted reply. UUID/hash equality makes a retransmission harmless;
// a different immutable request for the same UUID is rejected by the spool.
func PersistDispatch(ctx context.Context, store *spool.Store, session Session, dispatch *rvboxv1.CommandDispatch, now time.Time, limits agentproto.Limits) (spool.Acceptance, error) {
if store == nil || dispatch == nil || session.Generation == 0 || dispatch.GetTargetSessionGeneration() != session.Generation || now.IsZero() {
return spool.Acceptance{}, ErrProtocolHandshake
}
if err := agentproto.ValidateExecutionSpec(dispatch.GetSpec(), limits, rvboxv1.Platform_PLATFORM_WINDOWS); err != nil {
return spool.Acceptance{}, err
}
issue, err := domain.ParseUUIDv7(dispatch.GetIssueUuid())
if err != nil || dispatch.GetCommandRevision() == 0 || len(dispatch.GetImmutableRequestSha256()) != 32 {
return spool.Acceptance{}, ErrProtocolHandshake
}
var immutable [32]byte
copy(immutable[:], dispatch.GetImmutableRequestSha256())
if immutable == [32]byte{} {
return spool.Acceptance{}, errors.New("empty dispatch immutable hash")
}
return store.AcceptCommand(ctx, spool.Command{IssueUUID: issue, ImmutableSHA256: immutable, Revision: dispatch.GetCommandRevision(), Phase: uint32(rvboxv1.CommandLifecycle_COMMAND_ACCEPTED)}, now)
}
+19
View File
@@ -3,11 +3,13 @@ package agent
import ( import (
"context" "context"
"errors" "errors"
"path/filepath"
"testing" "testing"
"time" "time"
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
"github.com/rvbox/rvbox/internal/agentproto" "github.com/rvbox/rvbox/internal/agentproto"
"github.com/rvbox/rvbox/internal/client/spool"
"github.com/rvbox/rvbox/internal/domain" "github.com/rvbox/rvbox/internal/domain"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@@ -82,6 +84,23 @@ func TestApplyReconcileResult_HP_SES_12(t *testing.T) {
} }
} }
func TestPersistDispatchUsesImmutableHash_HP_DISPATCH_04(t *testing.T) {
store, err := spool.Open(context.Background(), spool.Options{DataDir: filepath.Join(t.TempDir(), "spool"), BusyTimeout: time.Second})
if err != nil {
t.Fatal(err)
}
defer store.Close()
dispatch := &rvboxv1.CommandDispatch{IssueUuid: "019c46f1-1d02-7000-8000-000000000064", CommandRevision: 1, TargetSessionGeneration: 7, IssueTime: timestamppb.Now(), ImmutableRequestSha256: []byte("12345678901234567890123456789012"), Spec: &rvboxv1.ExecutionSpec{ShellType: rvboxv1.ShellType_SHELL_POWERSHELL, Source: &rvboxv1.ExecutionSpec_CommandText{CommandText: "Write-Output ok"}}}
accepted, err := PersistDispatch(context.Background(), store, Session{Generation: 7}, dispatch, time.Now(), agentproto.DefaultLimits())
if err != nil || accepted.Duplicate {
t.Fatalf("PersistDispatch = %#v, %v", accepted, err)
}
duplicate, err := PersistDispatch(context.Background(), store, Session{Generation: 7}, dispatch, time.Now(), agentproto.DefaultLimits())
if err != nil || !duplicate.Duplicate {
t.Fatalf("duplicate PersistDispatch = %#v, %v", duplicate, err)
}
}
type fakeTransport struct { type fakeTransport struct {
written []byte written []byte
read []byte read []byte