From 981cd441fd9b3e7391031867ce1515c338017741 Mon Sep 17 00:00:00 2001 From: cabbage Date: Sun, 6 Sep 2026 09:17:41 +0000 Subject: [PATCH] feat: send canonical client command events --- internal/client/agent/event.go | 34 +++++++++++++++++++++++++ internal/client/agent/handshake_test.go | 17 +++++++++++++ test/coverage.toml | 6 +++++ 3 files changed, 57 insertions(+) create mode 100644 internal/client/agent/event.go diff --git a/internal/client/agent/event.go b/internal/client/agent/event.go new file mode 100644 index 0000000..71fd241 --- /dev/null +++ b/internal/client/agent/event.go @@ -0,0 +1,34 @@ +package agent + +import ( + "context" + "fmt" + + rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" + "github.com/rvbox/rvbox/internal/agentproto" + "google.golang.org/protobuf/proto" +) + +// SendCommandEvent writes a previously durable, sequence-assigned event. It +// derives the immutable digest at the final wire boundary so retrying the same +// event produces byte-equivalent protocol evidence. +func SendCommandEvent(ctx context.Context, transport Transport, session Session, event *rvboxv1.CommandEvent, limits agentproto.Limits) error { + if transport == nil || event == nil || session.ID == "" || session.Generation == 0 { + return ErrProtocolHandshake + } + copyEvent := proto.Clone(event).(*rvboxv1.CommandEvent) + digest, err := agentproto.CommandEventDigest(copyEvent) + if err != nil { + return err + } + copyEvent.ImmutableEventSha256 = digest[:] + envelope := &rvboxv1.AgentEnvelope{SessionId: session.ID, SessionGeneration: session.Generation, Payload: &rvboxv1.AgentEnvelope_CommandEvent{CommandEvent: copyEvent}} + if err := agentproto.ValidateEnvelope(envelope, limits, rvboxv1.Platform_PLATFORM_WINDOWS); err != nil { + return fmt.Errorf("validate command event: %w", err) + } + encoded, err := proto.Marshal(envelope) + if err != nil { + return err + } + return transport.Write(ctx, encoded) +} diff --git a/internal/client/agent/handshake_test.go b/internal/client/agent/handshake_test.go index 7eb6d1a..4b91f47 100644 --- a/internal/client/agent/handshake_test.go +++ b/internal/client/agent/handshake_test.go @@ -101,6 +101,23 @@ func TestPersistDispatchUsesImmutableHash_HP_DISPATCH_04(t *testing.T) { } } +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}}} + if err := SendCommandEvent(context.Background(), transport, Session{ID: "issued-session", Generation: 7}, event, agentproto.DefaultLimits()); err != nil { + t.Fatal(err) + } + var envelope rvboxv1.AgentEnvelope + if err := proto.Unmarshal(transport.written, &envelope); err != nil { + t.Fatal(err) + } + sent := envelope.GetCommandEvent() + digest, err := agentproto.CommandEventDigest(sent) + if err != nil || sent == nil || string(digest[:]) != string(sent.GetImmutableEventSha256()) { + t.Fatalf("sent event = %#v, %v", sent, err) + } +} + type fakeTransport struct { written []byte read []byte diff --git a/test/coverage.toml b/test/coverage.toml index 4574d63..d43ff6d 100644 --- a/test/coverage.toml +++ b/test/coverage.toml @@ -224,6 +224,12 @@ layer = "unit" status = "implemented" tests = ["internal/server/session/agent_server_test.go:TestWireEventAppendCarriesClientBinding_HP_EVENT_01"] +[[requirements]] +id = "HP-EVENT-02" +layer = "unit" +status = "implemented" +tests = ["internal/client/agent/handshake_test.go:TestSendCommandEventCanonicalDigest_HP_EVENT_02"] + [[requirements]] id = "HP-SES-05" layer = "integration"