feat: complete Windows client control and recovery paths

This commit is contained in:
2026-09-06 13:37:32 +00:00
parent 56b15c7f4f
commit 486894557d
38 changed files with 2188 additions and 106 deletions
+14 -5
View File
@@ -219,9 +219,8 @@ func (service *Service) GetCommand(ctx context.Context, request *rvboxv1.GetComm
return &rvboxv1.GetCommandResponse{Command: record}, nil
}
// RunCommand durably admits command-text work. Script payload persistence and
// chunk dispatch are intentionally kept behind the same immutable boundary and
// are rejected until their command_payloads path is wired to the dispatcher.
// RunCommand durably admits command-text and script work. Both forms share one
// immutable request boundary so replaying the same request ID is idempotent.
func (service *Service) RunCommand(ctx context.Context, request *rvboxv1.RunCommandRequest) (*rvboxv1.RunCommandResponse, error) {
if request == nil || request.GetTargetClientId() == "" || request.GetSpec() == nil {
return nil, controlError(codes.InvalidArgument, rvboxv1.ControlError_INVALID_ARGUMENT, "target_client_id and spec are required")
@@ -375,7 +374,10 @@ func (service *Service) GetOutput(ctx context.Context, request *rvboxv1.GetOutpu
if maxBytes > 16<<20 {
return nil, controlError(codes.InvalidArgument, rvboxv1.ControlError_INVALID_ARGUMENT, "max_bytes exceeds the control limit")
}
filterBytes := []byte(fmt.Sprintf("output\x00%s\x00%v", request.GetClientId(), streams))
// Bind the cursor to every selector that affects the result. In particular,
// issue_uuid must be included: otherwise a token issued for one command
// could be replayed against another command owned by the same client.
filterBytes := []byte(fmt.Sprintf("output\x00%s\x00%s\x00%v", request.GetClientId(), issue.String(), streams))
filter := domain.HashCursorFilters(filterBytes)
var eventSeq, offset, boundary uint64
if request.GetPageToken() != "" {
@@ -793,7 +795,14 @@ func commandRecord(view store.CommandView) (*rvboxv1.CommandRecord, error) {
if err := proto.Unmarshal(view.ExecutionSpec, spec); err != nil {
return nil, fmt.Errorf("decode stored execution spec: %w", err)
}
result := &rvboxv1.CommandRecord{IssueUuid: view.IssueUUID.String(), TargetClientId: view.ClientID, IssueTime: timestamppb.New(view.IssueTime), ServerReceiptTime: timestamppb.New(view.ServerReceiptTime), Spec: spec, Lifecycle: rvboxv1.CommandLifecycle(view.Lifecycle), LastEventSeq: view.LastEventSeq, OutputTruncated: view.OutputTruncated, OutputIncomplete: view.OutputIncomplete, RetainedCompressedBytes: view.RetainedCompressedBytes, CommandRevision: view.Revision}
result := &rvboxv1.CommandRecord{IssueUuid: view.IssueUUID.String(), TargetClientId: view.ClientID, IssueTime: timestamppb.New(view.IssueTime), ServerReceiptTime: timestamppb.New(view.ServerReceiptTime), Spec: spec, Lifecycle: rvboxv1.CommandLifecycle(view.Lifecycle), LastEventSeq: view.LastEventSeq, OutputTruncated: view.OutputTruncated, OutputIncomplete: view.OutputIncomplete, RetainedCompressedBytes: view.RetainedCompressedBytes, CommandRevision: view.Revision, LateAfterExpiry: view.LateAfterExpiry}
if len(view.Rejection) > 0 {
rejection := &rvboxv1.ControlError{}
if err := proto.Unmarshal(view.Rejection, rejection); err != nil {
return nil, fmt.Errorf("decode stored command rejection: %w", err)
}
result.Rejection = rejection
}
if view.QueueExpiryTime != nil {
result.QueueExpiryTime = timestamppb.New(*view.QueueExpiryTime)
}
+4
View File
@@ -240,6 +240,10 @@ func TestGetOutputSlicesWithAuthenticatedCursor_HP_CONTROL_12(t *testing.T) {
if _, err := service.GetOutput(context.Background(), &rvboxv1.GetOutputRequest{ClientId: "win-a", IssueUuid: issue.String(), Streams: []rvboxv1.StreamKind{rvboxv1.StreamKind_STREAM_STDERR}, MaxBytes: 3, PageToken: first.GetNextPageToken()}); status.Code(err) != codes.InvalidArgument {
t.Fatalf("changed output filter code = %v", status.Code(err))
}
otherIssue := fixedIssue(0xa8)
if _, err := service.GetOutput(context.Background(), &rvboxv1.GetOutputRequest{ClientId: "win-a", IssueUuid: otherIssue.String(), Streams: []rvboxv1.StreamKind{rvboxv1.StreamKind_STREAM_STDOUT}, MaxBytes: 3, PageToken: first.GetNextPageToken()}); status.Code(err) != codes.InvalidArgument {
t.Fatalf("changed output command code = %v", status.Code(err))
}
}
func TestControlMutationIdempotencyAndQueuedCancellation_BH_CONTROL_13(t *testing.T) {