[infra][ftx] Updates ftxclient to wait for task completion and respect more input options.

Bug: b/258456267
Change-Id: Ie3509775346b18db7ba3e8c38c291de7a9cafbbe
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/864806
Reviewed-by: Rahul Bangar <rahulbn@google.com>
Commit-Queue: Vinicius Felizardo <felizardo@google.com>
diff --git a/cmd/ftxtest/proto/input.pb.go b/cmd/ftxtest/proto/input.pb.go
index 816d45e..2df3a35 100644
--- a/cmd/ftxtest/proto/input.pb.go
+++ b/cmd/ftxtest/proto/input.pb.go
@@ -33,14 +33,11 @@
 	// and run tests.
 	InputArtifactsDigest string `protobuf:"bytes,1,opt,name=input_artifacts_digest,json=inputArtifactsDigest,proto3" json:"input_artifacts_digest,omitempty"`
 	// Any CIPD packages needed to run the test task.
-	CipdPackages []*CIPDPackage `protobuf:"bytes,2,rep,name=cipd_packages,json=cipdPackages,proto3" json:"cipd_packages,omitempty"`
-	// Parameters controlling how the test target and the test environment are prepared.
-	PreprationParams *PreparationParams `protobuf:"bytes,3,opt,name=prepration_params,json=preprationParams,proto3" json:"prepration_params,omitempty"`
-	// The test command to run. The test binary should be in the input artifacts.
-	TestCommand string `protobuf:"bytes,4,opt,name=test_command,json=testCommand,proto3" json:"test_command,omitempty"`
-	// Test targeting metadata used to select devices/emulators to run on.
-	// These will be discussed more comprehensively later.
+	CipdPackages []*CipdPackage `protobuf:"bytes,2,rep,name=cipd_packages,json=cipdPackages,proto3" json:"cipd_packages,omitempty"`
+	// Dimensions to select which swarming bots to target.
 	TargetDimensions map[string]string `protobuf:"bytes,5,rep,name=target_dimensions,json=targetDimensions,proto3" json:"target_dimensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// Test execution details.
+	Test *GenericTest `protobuf:"bytes,8,opt,name=test,proto3" json:"test,omitempty"`
 }
 
 func (x *InputProperties) Reset() {
@@ -96,27 +93,13 @@
 	return ""
 }
 
-func (x *InputProperties) GetCipdPackages() []*CIPDPackage {
+func (x *InputProperties) GetCipdPackages() []*CipdPackage {
 	if x != nil {
 		return x.CipdPackages
 	}
 	return nil
 }
 
-func (x *InputProperties) GetPreprationParams() *PreparationParams {
-	if x != nil {
-		return x.PreprationParams
-	}
-	return nil
-}
-
-func (x *InputProperties) GetTestCommand() string {
-	if x != nil {
-		return x.TestCommand
-	}
-	return ""
-}
-
 func (x *InputProperties) GetTargetDimensions() map[string]string {
 	if x != nil {
 		return x.TargetDimensions
@@ -124,17 +107,26 @@
 	return nil
 }
 
-type CIPDPackage struct {
+func (x *InputProperties) GetTest() *GenericTest {
+	if x != nil {
+		return x.Test
+	}
+	return nil
+}
+
+type HostTest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
-	Tag  string `protobuf:"bytes,2,opt,name=tag,proto3" json:"tag,omitempty"`
+	// Command.
+	Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"`
+	// System environment variables.
+	Env map[string]string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
 }
 
-func (x *CIPDPackage) Reset() {
-	*x = CIPDPackage{}
+func (x *HostTest) Reset() {
+	*x = HostTest{}
 	if protoimpl.UnsafeEnabled {
 		mi := &file_proto_input_proto_msgTypes[1]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -142,13 +134,13 @@
 	}
 }
 
-func (x *CIPDPackage) String() string {
+func (x *HostTest) String() string {
 	return protoimpl.X.MessageStringOf(x)
 }
 
-func (*CIPDPackage) ProtoMessage() {}
+func (*HostTest) ProtoMessage() {}
 
-func (x *CIPDPackage) ProtoReflect() protoreflect.Message {
+func (x *HostTest) ProtoReflect() protoreflect.Message {
 	mi := &file_proto_input_proto_msgTypes[1]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -160,39 +152,38 @@
 	return mi.MessageOf(x)
 }
 
-// Deprecated: Use CIPDPackage.ProtoReflect.Descriptor instead.
-func (*CIPDPackage) Descriptor() ([]byte, []int) {
+// Deprecated: Use HostTest.ProtoReflect.Descriptor instead.
+func (*HostTest) Descriptor() ([]byte, []int) {
 	return file_proto_input_proto_rawDescGZIP(), []int{1}
 }
 
-func (x *CIPDPackage) GetPath() string {
+func (x *HostTest) GetCommand() []string {
 	if x != nil {
-		return x.Path
+		return x.Command
 	}
-	return ""
+	return nil
 }
 
-func (x *CIPDPackage) GetTag() string {
+func (x *HostTest) GetEnv() map[string]string {
 	if x != nil {
-		return x.Tag
+		return x.Env
 	}
-	return ""
+	return nil
 }
 
-type PreparationParams struct {
+type GenericTest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Flash            bool `protobuf:"varint,1,opt,name=flash,proto3" json:"flash,omitempty"`
-	Pave             bool `protobuf:"varint,2,opt,name=pave,proto3" json:"pave,omitempty"`
-	Ramboot          bool `protobuf:"varint,3,opt,name=ramboot,proto3" json:"ramboot,omitempty"`
-	RunPackageServer bool `protobuf:"varint,4,opt,name=run_package_server,json=runPackageServer,proto3" json:"run_package_server,omitempty"`
-	CaptureLogs      bool `protobuf:"varint,5,opt,name=capture_logs,json=captureLogs,proto3" json:"capture_logs,omitempty"`
+	// Types that are assignable to Kind:
+	//
+	//	*GenericTest_Host
+	Kind isGenericTest_Kind `protobuf_oneof:"kind"`
 }
 
-func (x *PreparationParams) Reset() {
-	*x = PreparationParams{}
+func (x *GenericTest) Reset() {
+	*x = GenericTest{}
 	if protoimpl.UnsafeEnabled {
 		mi := &file_proto_input_proto_msgTypes[2]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -200,13 +191,13 @@
 	}
 }
 
-func (x *PreparationParams) String() string {
+func (x *GenericTest) String() string {
 	return protoimpl.X.MessageStringOf(x)
 }
 
-func (*PreparationParams) ProtoMessage() {}
+func (*GenericTest) ProtoMessage() {}
 
-func (x *PreparationParams) ProtoReflect() protoreflect.Message {
+func (x *GenericTest) ProtoReflect() protoreflect.Message {
 	mi := &file_proto_input_proto_msgTypes[2]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -218,51 +209,107 @@
 	return mi.MessageOf(x)
 }
 
-// Deprecated: Use PreparationParams.ProtoReflect.Descriptor instead.
-func (*PreparationParams) Descriptor() ([]byte, []int) {
+// Deprecated: Use GenericTest.ProtoReflect.Descriptor instead.
+func (*GenericTest) Descriptor() ([]byte, []int) {
 	return file_proto_input_proto_rawDescGZIP(), []int{2}
 }
 
-func (x *PreparationParams) GetFlash() bool {
-	if x != nil {
-		return x.Flash
+func (m *GenericTest) GetKind() isGenericTest_Kind {
+	if m != nil {
+		return m.Kind
 	}
-	return false
+	return nil
 }
 
-func (x *PreparationParams) GetPave() bool {
-	if x != nil {
-		return x.Pave
+func (x *GenericTest) GetHost() *HostTest {
+	if x, ok := x.GetKind().(*GenericTest_Host); ok {
+		return x.Host
 	}
-	return false
+	return nil
 }
 
-func (x *PreparationParams) GetRamboot() bool {
-	if x != nil {
-		return x.Ramboot
-	}
-	return false
+type isGenericTest_Kind interface {
+	isGenericTest_Kind()
 }
 
-func (x *PreparationParams) GetRunPackageServer() bool {
-	if x != nil {
-		return x.RunPackageServer
-	}
-	return false
+type GenericTest_Host struct {
+	// Runs arbitrary binaries directly on the host environment.
+	Host *HostTest `protobuf:"bytes,1,opt,name=host,proto3,oneof"`
 }
 
-func (x *PreparationParams) GetCaptureLogs() bool {
-	if x != nil {
-		return x.CaptureLogs
+func (*GenericTest_Host) isGenericTest_Kind() {}
+
+type CipdPackage struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Name of the cipd package, i.e. fuchsia/sdk/core/${platform}
+	Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+	// Version of the cipd package to install, i.e. latest
+	Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"`
+	// Path to install cipd package to, i.e. sdk
+	Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+}
+
+func (x *CipdPackage) Reset() {
+	*x = CipdPackage{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_proto_input_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
 	}
-	return false
+}
+
+func (x *CipdPackage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CipdPackage) ProtoMessage() {}
+
+func (x *CipdPackage) ProtoReflect() protoreflect.Message {
+	mi := &file_proto_input_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CipdPackage.ProtoReflect.Descriptor instead.
+func (*CipdPackage) Descriptor() ([]byte, []int) {
+	return file_proto_input_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CipdPackage) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *CipdPackage) GetVersion() string {
+	if x != nil {
+		return x.Version
+	}
+	return ""
+}
+
+func (x *CipdPackage) GetPath() string {
+	if x != nil {
+		return x.Path
+	}
+	return ""
 }
 
 var File_proto_input_proto protoreflect.FileDescriptor
 
 var file_proto_input_proto_rawDesc = []byte{
 	0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x22, 0xa8, 0x03, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x50, 0x72, 0x6f,
+	0x6f, 0x74, 0x6f, 0x22, 0xe6, 0x02, 0x0a, 0x0f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x50, 0x72, 0x6f,
 	0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
 	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65,
 	0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65,
@@ -271,41 +318,40 @@
 	0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x41, 0x72,
 	0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a,
 	0x0d, 0x63, 0x69, 0x70, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02,
-	0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x43, 0x49, 0x50, 0x44, 0x50, 0x61, 0x63, 0x6b, 0x61,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x43, 0x69, 0x70, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61,
 	0x67, 0x65, 0x52, 0x0c, 0x63, 0x69, 0x70, 0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73,
-	0x12, 0x3f, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x70, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70,
-	0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x50, 0x72,
-	0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52,
-	0x10, 0x70, 0x72, 0x65, 0x70, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d,
-	0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
-	0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d,
-	0x6d, 0x61, 0x6e, 0x64, 0x12, 0x53, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x64,
-	0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32,
-	0x26, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65,
-	0x73, 0x2e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f,
-	0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x44,
-	0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x43, 0x0a, 0x15, 0x54, 0x61, 0x72,
-	0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74,
-	0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x33,
-	0x0a, 0x0b, 0x43, 0x49, 0x50, 0x44, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a,
-	0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74,
-	0x68, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
-	0x74, 0x61, 0x67, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74,
-	0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61,
-	0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x12,
-	0x12, 0x0a, 0x04, 0x70, 0x61, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x70,
-	0x61, 0x76, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x61, 0x6d, 0x62, 0x6f, 0x6f, 0x74, 0x18, 0x03,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x61, 0x6d, 0x62, 0x6f, 0x6f, 0x74, 0x12, 0x2c, 0x0a,
-	0x12, 0x72, 0x75, 0x6e, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x65, 0x72,
-	0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x75, 0x6e, 0x50, 0x61,
-	0x63, 0x6b, 0x61, 0x67, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x63,
-	0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x0b, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x28,
-	0x5a, 0x26, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76,
-	0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x74, 0x78, 0x74, 0x65,
-	0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x12, 0x53, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e,
+	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x49, 0x6e,
+	0x70, 0x75, 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x54, 0x61,
+	0x72, 0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e,
+	0x74, 0x72, 0x79, 0x52, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e,
+	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x65, 0x73, 0x74, 0x18, 0x08, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x54, 0x65, 0x73,
+	0x74, 0x52, 0x04, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x0a, 0x15, 0x54, 0x61, 0x72, 0x67, 0x65,
+	0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
+	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
+	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x82, 0x01, 0x0a,
+	0x08, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d,
+	0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
+	0x61, 0x6e, 0x64, 0x12, 0x24, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x12, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x45,
+	0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, 0x76,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+	0x01, 0x22, 0x36, 0x0a, 0x0b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x54, 0x65, 0x73, 0x74,
+	0x12, 0x1f, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09,
+	0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x68, 0x6f, 0x73,
+	0x74, 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x4f, 0x0a, 0x0b, 0x43, 0x69, 0x70,
+	0x64, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x6f,
+	0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66,
+	0x72, 0x61, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x74, 0x78, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -320,22 +366,26 @@
 	return file_proto_input_proto_rawDescData
 }
 
-var file_proto_input_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_proto_input_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
 var file_proto_input_proto_goTypes = []interface{}{
-	(*InputProperties)(nil),   // 0: InputProperties
-	(*CIPDPackage)(nil),       // 1: CIPDPackage
-	(*PreparationParams)(nil), // 2: PreparationParams
-	nil,                       // 3: InputProperties.TargetDimensionsEntry
+	(*InputProperties)(nil), // 0: InputProperties
+	(*HostTest)(nil),        // 1: HostTest
+	(*GenericTest)(nil),     // 2: GenericTest
+	(*CipdPackage)(nil),     // 3: CipdPackage
+	nil,                     // 4: InputProperties.TargetDimensionsEntry
+	nil,                     // 5: HostTest.EnvEntry
 }
 var file_proto_input_proto_depIdxs = []int32{
-	1, // 0: InputProperties.cipd_packages:type_name -> CIPDPackage
-	2, // 1: InputProperties.prepration_params:type_name -> PreparationParams
-	3, // 2: InputProperties.target_dimensions:type_name -> InputProperties.TargetDimensionsEntry
-	3, // [3:3] is the sub-list for method output_type
-	3, // [3:3] is the sub-list for method input_type
-	3, // [3:3] is the sub-list for extension type_name
-	3, // [3:3] is the sub-list for extension extendee
-	0, // [0:3] is the sub-list for field type_name
+	3, // 0: InputProperties.cipd_packages:type_name -> CipdPackage
+	4, // 1: InputProperties.target_dimensions:type_name -> InputProperties.TargetDimensionsEntry
+	2, // 2: InputProperties.test:type_name -> GenericTest
+	5, // 3: HostTest.env:type_name -> HostTest.EnvEntry
+	1, // 4: GenericTest.host:type_name -> HostTest
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
 }
 
 func init() { file_proto_input_proto_init() }
@@ -357,7 +407,7 @@
 			}
 		}
 		file_proto_input_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*CIPDPackage); i {
+			switch v := v.(*HostTest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -369,7 +419,19 @@
 			}
 		}
 		file_proto_input_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*PreparationParams); i {
+			switch v := v.(*GenericTest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_proto_input_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CipdPackage); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -381,13 +443,16 @@
 			}
 		}
 	}
+	file_proto_input_proto_msgTypes[2].OneofWrappers = []interface{}{
+		(*GenericTest_Host)(nil),
+	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_proto_input_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   4,
+			NumMessages:   6,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/cmd/ftxtest/proto/input.proto b/cmd/ftxtest/proto/input.proto
index 076ccf8..4e5da9d 100644
--- a/cmd/ftxtest/proto/input.proto
+++ b/cmd/ftxtest/proto/input.proto
@@ -11,25 +11,32 @@
         // and run tests.
         string input_artifacts_digest = 1;
         // Any CIPD packages needed to run the test task.
-        repeated CIPDPackage cipd_packages = 2;
-        // Parameters controlling how the test target and the test environment are prepared.
-        PreparationParams prepration_params = 3;
-        // The test command to run. The test binary should be in the input artifacts.
-        string test_command = 4;
-        // Test targeting metadata used to select devices/emulators to run on.
-        // These will be discussed more comprehensively later.
+        repeated CipdPackage cipd_packages = 2;
+        // Dimensions to select which swarming bots to target.
         map<string, string> target_dimensions = 5;
+        // Test execution details.
+        GenericTest test = 8;
 }
 
-message CIPDPackage {
+message HostTest {
+        // Command.
+        repeated string command = 1;
+        // System environment variables.
+        map<string, string> env = 2;
+}
+
+message GenericTest {
+    oneof kind {
+        // Runs arbitrary binaries directly on the host environment.
+        HostTest host = 1;
+    }
+}
+
+message CipdPackage {
+        // Name of the cipd package, i.e. fuchsia/sdk/core/${platform}
+        string name = 3;
+        // Version of the cipd package to install, i.e. latest
+        string version = 2;
+        // Path to install cipd package to, i.e. sdk
         string path = 1;
-        string tag = 2;
-}
-
-message PreparationParams {
-        bool flash = 1;
-        bool pave = 2;
-        bool ramboot = 3;
-        bool run_package_server = 4;
-        bool capture_logs = 5;
-}
+}
\ No newline at end of file
diff --git a/cmd/ftxtest/run.go b/cmd/ftxtest/run.go
index c0a8589..e08fedc 100644
--- a/cmd/ftxtest/run.go
+++ b/cmd/ftxtest/run.go
@@ -7,7 +7,6 @@
 	"context"
 	"errors"
 	"fmt"
-	"net/http"
 	"os"
 	"strings"
 
@@ -55,41 +54,66 @@
 	buildInput := &ftxproto.InputProperties{}
 	var writeOutputProps func(*any.Any)
 	build.Main(buildInput, &writeOutputProps, nil, func(ctx context.Context, extraArgs []string, state *build.State) error {
-		if len(buildInput.Name) == 0 {
-			return errors.New("Name is required.")
-		}
-		authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, r.parsedAuthOpts)
-		httpClient, err := authenticator.Client()
+		swarming, err := r.authenticateStep(ctx, buildInput)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "You need to login first by running:\n")
-			fmt.Fprintf(os.Stderr, "  luci-auth login -scopes %q\n", strings.Join(r.parsedAuthOpts.Scopes, " "))
-			return errors.New("Not logged in.")
+			return fmt.Errorf("authenticateStep: %v", err)
 		}
-		_, _, err = LaunchTaskStep(ctx, httpClient, buildInput)
+		taskId, err := r.launchTaskStep(ctx, swarming, buildInput)
 		if err != nil {
-			return fmt.Errorf("LaunchTaskStep: %v", err)
+			return fmt.Errorf("launchTaskStep: %v", err)
+		}
+		if err := r.waitTaskStep(ctx, swarming, taskId); err != nil {
+			return fmt.Errorf("waitTaskStep: %v", err)
 		}
 		return nil
 	})
 }
 
-func LaunchTaskStep(ctx context.Context, httpClient *http.Client, buildInput *ftxproto.InputProperties) (*Swarming, string, error) {
-	step, ctx := build.StartStep(ctx, "Launch Swarming Task")
+func (r *runImpl) authenticateStep(ctx context.Context, buildInput *ftxproto.InputProperties) (*Swarming, error) {
+	step, ctx := build.StartStep(ctx, "Authenticate")
+	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, r.parsedAuthOpts)
+	httpClient, err := authenticator.Client()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "You need to login first by running:\n")
+		fmt.Fprintf(os.Stderr, "  luci-auth login -scopes %q\n", strings.Join(r.parsedAuthOpts.Scopes, " "))
+		return nil, errors.New("Not logged in.")
+	}
 	instance := instance(buildInput)
 	swarming, err := NewSwarming(ctx, httpClient, instance)
 	if err != nil {
 		step.End(err)
-		return nil, "", fmt.Errorf("NewSwarming: %v", err)
+		return nil, fmt.Errorf("NewSwarming: %v", err)
+	}
+	step.End(nil)
+	return swarming, nil
+}
+
+func (r *runImpl) launchTaskStep(ctx context.Context, swarming *Swarming, buildInput *ftxproto.InputProperties) (string, error) {
+	step, ctx := build.StartStep(ctx, "Launch Swarming Task")
+	if len(buildInput.Name) == 0 {
+		err := errors.New("Name is required.")
+		step.End(err)
+		return "", err
 	}
 	task, err := swarming.LaunchTask(buildInput)
 	if err != nil {
 		step.End(err)
-		return nil, "", fmt.Errorf("LaunchTask: %v", err)
+		return "", fmt.Errorf("LaunchTask: %v", err)
 	}
-	md := fmt.Sprintf("* [swarming task](https://%s.appspot.com/task?id=%s)", instance, task.TaskId)
+	md := fmt.Sprintf("* [swarming task](https://%s.appspot.com/task?id=%s)", instance(buildInput), task.TaskId)
 	step.SetSummaryMarkdown(md)
 	step.End(nil)
-	return swarming, task.TaskId, nil
+	return task.TaskId, nil
+}
+
+func (r *runImpl) waitTaskStep(ctx context.Context, swarming *Swarming, taskId string) error {
+	step, ctx := build.StartStep(ctx, "Wait Task Completion")
+	if err := swarming.WaitTask(taskId); err != nil {
+		step.End(err)
+		return err
+	}
+	step.End(nil)
+	return nil
 }
 
 func instance(buildInput *ftxproto.InputProperties) string {
diff --git a/cmd/ftxtest/swarming.go b/cmd/ftxtest/swarming.go
index 0f4f61c..8197945 100644
--- a/cmd/ftxtest/swarming.go
+++ b/cmd/ftxtest/swarming.go
@@ -5,6 +5,7 @@
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -24,6 +25,7 @@
 	taskPriority         = 200
 	taskExpiration       = 5 * time.Hour
 	taskExecutionTimeout = 2 * time.Hour
+	poolTaskInterval     = 10 * time.Second
 )
 
 func NewSwarming(ctx context.Context, httpClient *http.Client, instance string) (*Swarming, error) {
@@ -44,6 +46,7 @@
 	if err != nil {
 		return nil, fmt.Errorf("casInput: %v", err)
 	}
+	hostTest := buildInput.Test.GetHost()
 	return s.service.Tasks.New(&swarming.SwarmingRpcsNewTaskRequest{
 		Name:           buildInput.Name,
 		ExpirationSecs: int64(taskExpiration.Seconds()),
@@ -51,13 +54,34 @@
 		Realm:          realm(buildInput),
 		Properties: &swarming.SwarmingRpcsTaskProperties{
 			ExecutionTimeoutSecs: int64(taskExpiration.Seconds()),
-			Command:              []string{buildInput.TestCommand},
+			Command:              hostTest.Command,
 			CasInputRoot:         casInput,
-			Dimensions:           dimensions(buildInput),
+			CipdInput:            cipdInput(buildInput.CipdPackages),
+			Dimensions:           swarmmingStringPair(buildInput.TargetDimensions),
+			Env:                  swarmmingStringPair(hostTest.Env),
 		},
 	}).Do()
 }
 
+func (s *Swarming) WaitTask(taskId string) error {
+	for {
+		result, err := s.service.Task.Result(taskId).Fields("state", "failure", "internal_failure").Do()
+		if err != nil {
+			err = fmt.Errorf("swarming.Task.Result RPC failed: %v", err)
+			return err
+		}
+		if result.Failure || result.InternalFailure {
+			err = errors.New("Swarming task failed")
+			return err
+		}
+		if result.State == "PENDING" || result.State == "RUNNING" {
+			time.Sleep(poolTaskInterval)
+			continue
+		}
+		return nil
+	}
+}
+
 func (s *Swarming) casInput(buildInput *ftxproto.InputProperties) (*swarming.SwarmingRpcsCASReference, error) {
 	digestSplit := strings.Split(buildInput.InputArtifactsDigest, "/")
 	sizeBytes, err := strconv.ParseInt(digestSplit[1], 10, 64)
@@ -73,9 +97,23 @@
 	}, nil
 }
 
-func dimensions(buildInput *ftxproto.InputProperties) []*swarming.SwarmingRpcsStringPair {
+func cipdInput(cipdPackages []*ftxproto.CipdPackage) *swarming.SwarmingRpcsCipdInput {
+	packages := []*swarming.SwarmingRpcsCipdPackage{}
+	for _, inputPkg := range cipdPackages {
+		packages = append(packages, &swarming.SwarmingRpcsCipdPackage{
+			Path:        inputPkg.Path,
+			Version:     inputPkg.Version,
+			PackageName: inputPkg.Name,
+		})
+	}
+	return &swarming.SwarmingRpcsCipdInput{
+		Packages: packages,
+	}
+}
+
+func swarmmingStringPair(m map[string]string) []*swarming.SwarmingRpcsStringPair {
 	result := []*swarming.SwarmingRpcsStringPair{}
-	for key, value := range buildInput.TargetDimensions {
+	for key, value := range m {
 		result = append(result, &swarming.SwarmingRpcsStringPair{
 			Key:   key,
 			Value: value,
diff --git a/cmd/ftxtest/test/build.json b/cmd/ftxtest/test/build.json
index a3dff20..f97c529 100644
--- a/cmd/ftxtest/test/build.json
+++ b/cmd/ftxtest/test/build.json
@@ -3,10 +3,24 @@
         "properties": {
             "name": "hello_swarming_test",
             "external": false,
-            "input_artifacts_digest": "11ab3e1a0ba7bc9fbd6957c57222331f340dc981ec2bcb5edeffc05014e7a2ba/83",
-            "test_command": "turquoise/infra/foundation/go/ftxclient/examples/hello_swarming/hello_swarming.par",
+            "input_artifacts_digest": "19a21c55f6790bc5e07f9e6c3be64dfd19fa696feaed629c6a9a8e15e5e469cb/83",
             "target_dimensions": {
                 "pool": "fuchsia.dev.tests"
+            },
+            "cipd_packages": [
+                {
+                    "name": "fuchsia/sdk/core/${platform}",
+                    "path": "sdk",
+                    "version": "git_revision:667a4d316e06b8ec073085c1af0596aeb23b4544"
+                }
+            ],
+            "test": {
+                "host": {
+                    "command": ["turquoise/infra/foundation/go/ftxclient/examples/hello_swarming/hello_swarming"],
+                    "env": {
+                        "foo": "bar"
+                    }
+                }
             }
         }
     }