feat: persist and transfer script command payloads
This commit is contained in:
@@ -35,5 +35,15 @@ func PersistDispatch(ctx context.Context, store *spool.Store, session Session, d
|
||||
if err != nil {
|
||||
return spool.Acceptance{}, err
|
||||
}
|
||||
return store.AcceptCommand(ctx, spool.Command{IssueUUID: issue, ImmutableSHA256: immutable, Revision: dispatch.GetCommandRevision(), Phase: uint32(rvboxv1.CommandLifecycle_COMMAND_ACCEPTED), ExecutionSpec: spec}, now)
|
||||
acceptance, err := store.AcceptCommand(ctx, spool.Command{IssueUUID: issue, ImmutableSHA256: immutable, Revision: dispatch.GetCommandRevision(), Phase: uint32(rvboxv1.CommandLifecycle_COMMAND_ACCEPTED), ExecutionSpec: spec}, now)
|
||||
if err != nil {
|
||||
return spool.Acceptance{}, err
|
||||
}
|
||||
if descriptor := dispatch.GetSpec().GetScript(); descriptor != nil {
|
||||
_, err = store.BeginScript(ctx, issue, spool.ScriptDescriptor{SizeBytes: descriptor.GetSizeBytes(), SHA256: bytesToDigest(descriptor.GetSha256())})
|
||||
if err != nil {
|
||||
return spool.Acceptance{}, err
|
||||
}
|
||||
}
|
||||
return acceptance, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -103,6 +104,42 @@ func TestPersistDispatchUsesImmutableHash_HP_DISPATCH_04(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptFramesApplyDurablyAndReplaySafely_HP_SCRIPT_04(t *testing.T) {
|
||||
store, err := spool.Open(context.Background(), spool.Options{DataDir: filepath.Join(t.TempDir(), "script-spool"), BusyTimeout: time.Second})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
body := []byte("Write-Output 'hello'\r\n")
|
||||
digest := sha256.Sum256(body)
|
||||
dispatch := &rvboxv1.CommandDispatch{IssueUuid: "019c46f1-1d02-7000-8000-000000000067", CommandRevision: 1, TargetSessionGeneration: 9, IssueTime: timestamppb.Now(), ImmutableRequestSha256: []byte("12345678901234567890123456789012"), Spec: &rvboxv1.ExecutionSpec{ShellType: rvboxv1.ShellType_SHELL_POWERSHELL, Source: &rvboxv1.ExecutionSpec_Script{Script: &rvboxv1.ScriptDescriptor{Filename: "hello.ps1", SizeBytes: uint64(len(body)), Sha256: digest[:]}}}}
|
||||
accepted, err := PersistDispatch(context.Background(), store, Session{Generation: 9}, dispatch, time.Now().UTC(), agentproto.DefaultLimits())
|
||||
if err != nil || accepted.Duplicate {
|
||||
t.Fatalf("script PersistDispatch = %#v, %v", accepted, err)
|
||||
}
|
||||
chunkDigest := sha256.Sum256(body[:8])
|
||||
status, err := ApplyScriptChunk(context.Background(), store, Session{Generation: 9}, &rvboxv1.ScriptChunk{IssueUuid: dispatch.GetIssueUuid(), Offset: 0, Data: body[:8], Sha256: chunkDigest[:]}, agentproto.DefaultLimits())
|
||||
if err != nil || status.ReceivedBytes != 8 {
|
||||
t.Fatalf("first script chunk = %#v, %v", status, err)
|
||||
}
|
||||
duplicate, err := ApplyScriptChunk(context.Background(), store, Session{Generation: 9}, &rvboxv1.ScriptChunk{IssueUuid: dispatch.GetIssueUuid(), Offset: 0, Data: body[:8], Sha256: chunkDigest[:]}, agentproto.DefaultLimits())
|
||||
if err != nil || !duplicate.Duplicate || duplicate.ReceivedBytes != 8 {
|
||||
t.Fatalf("replayed script chunk = %#v, %v", duplicate, err)
|
||||
}
|
||||
restDigest := sha256.Sum256(body[8:])
|
||||
if _, err := ApplyScriptChunk(context.Background(), store, Session{Generation: 9}, &rvboxv1.ScriptChunk{IssueUuid: dispatch.GetIssueUuid(), Offset: 8, Data: body[8:], Sha256: restDigest[:]}, agentproto.DefaultLimits()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
committed, err := ApplyScriptCommit(context.Background(), store, Session{Generation: 9}, &rvboxv1.ScriptCommit{IssueUuid: dispatch.GetIssueUuid(), SizeBytes: uint64(len(body)), Sha256: digest[:]}, agentproto.DefaultLimits())
|
||||
if err != nil || !committed.Committed || committed.ReceivedBytes != uint64(len(body)) {
|
||||
t.Fatalf("script commit = %#v, %v", committed, err)
|
||||
}
|
||||
replayed, err := ApplyScriptChunk(context.Background(), store, Session{Generation: 9}, &rvboxv1.ScriptChunk{IssueUuid: dispatch.GetIssueUuid(), Offset: 8, Data: body[8:], Sha256: restDigest[:]}, agentproto.DefaultLimits())
|
||||
if err != nil || !replayed.Duplicate || !replayed.Committed {
|
||||
t.Fatalf("post-commit chunk replay = %#v, %v", replayed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendCommandEventCanonicalDigest_HP_EVENT_02(t *testing.T) {
|
||||
transport := &fakeTransport{}
|
||||
event := &rvboxv1.CommandEvent{IssueUuid: "019c46f1-1d02-7000-8000-000000000065", EventSeq: 1, ObservedAt: timestamppb.Now(), Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle_COMMAND_RUNNING, CommandRevision: 1}}}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var ErrInvalidScriptTransfer = errors.New("invalid script transfer")
|
||||
|
||||
// ApplyScriptChunk validates one server script frame and appends it to the
|
||||
// durable client spool. The spool accepts only the next contiguous range or
|
||||
// an exact replay, so reconnects cannot create holes or duplicate bytes.
|
||||
func ApplyScriptChunk(ctx context.Context, store *spool.Store, session Session, chunk *rvboxv1.ScriptChunk, limits agentproto.Limits) (spool.ScriptStatus, error) {
|
||||
if store == nil || session.Generation == 0 || chunk == nil || chunk.GetOffset() > limits.MaxScriptBytes || uint64(len(chunk.GetData())) > limits.MaxRawChunkBytes || len(chunk.GetSha256()) != sha256.Size {
|
||||
return spool.ScriptStatus{}, ErrInvalidScriptTransfer
|
||||
}
|
||||
issue, err := domain.ParseUUIDv7(chunk.GetIssueUuid())
|
||||
if err != nil || sha256.Sum256(chunk.GetData()) != bytesToDigest(chunk.GetSha256()) {
|
||||
return spool.ScriptStatus{}, ErrInvalidScriptTransfer
|
||||
}
|
||||
return store.AppendScriptChunk(ctx, issue, chunk.GetOffset(), chunk.GetData(), bytesToDigest(chunk.GetSha256()))
|
||||
}
|
||||
|
||||
// ApplyScriptCommit marks a fully received script launch-eligible only after
|
||||
// the spool verifies the declared size and whole-body SHA-256.
|
||||
func ApplyScriptCommit(ctx context.Context, store *spool.Store, session Session, commit *rvboxv1.ScriptCommit, limits agentproto.Limits) (spool.ScriptStatus, error) {
|
||||
if store == nil || session.Generation == 0 || commit == nil || commit.GetSizeBytes() > limits.MaxScriptBytes || len(commit.GetSha256()) != sha256.Size {
|
||||
return spool.ScriptStatus{}, ErrInvalidScriptTransfer
|
||||
}
|
||||
issue, err := domain.ParseUUIDv7(commit.GetIssueUuid())
|
||||
if err != nil {
|
||||
return spool.ScriptStatus{}, ErrInvalidScriptTransfer
|
||||
}
|
||||
return store.CommitScript(ctx, issue, spool.ScriptDescriptor{SizeBytes: commit.GetSizeBytes(), SHA256: bytesToDigest(commit.GetSha256())})
|
||||
}
|
||||
|
||||
func bytesToDigest(value []byte) [sha256.Size]byte {
|
||||
var digest [sha256.Size]byte
|
||||
copy(digest[:], value)
|
||||
return digest
|
||||
}
|
||||
Reference in New Issue
Block a user