diff --git a/src/bitdrift_public/protobuf/bdtail/v1/bdtail_config.proto b/src/bitdrift_public/protobuf/bdtail/v1/bdtail_config.proto index e3419a3..b9ae61b 100644 --- a/src/bitdrift_public/protobuf/bdtail/v1/bdtail_config.proto +++ b/src/bitdrift_public/protobuf/bdtail/v1/bdtail_config.proto @@ -12,6 +12,36 @@ package bitdrift_public.protobuf.bdtail.v1; import "bitdrift_public/protobuf/matcher/v1/log_matcher.proto"; import "validate/validate.proto"; +// Requests that a device execute a command through a BDTail configuration. +message DeviceCommandRequest { + // Requests the device's buffered logs. + message DumpDeviceBufferCommand {} + + // Requests the device's current screen as a JPEG image. + message TakeScreenshotCommand {} + + // Requests an opaque command handled by a client-registered definition. + message CustomCommand { + string command_definition_id = 1 [(validate.rules).string.uuid = true]; + } + + // The command ID generated by the server. + string command_id = 1 [(validate.rules).string.uuid = true]; + + oneof command_type { + option (validate.required) = true; + + // Requests the device's buffered logs. + DumpDeviceBufferCommand dump_device_buffer = 2; + + // Requests the device's current screen as a JPEG image. + TakeScreenshotCommand take_screenshot = 3; + + // Requests an opaque client-registered command. + CustomCommand custom_command = 4; + } +} + message BdTailStream { // The name of the bdtail stream. Logs that match the matcher below should be annotated with this // stream ID to allow the SaaS to associate the log with the bdtail stream. @@ -20,6 +50,9 @@ message BdTailStream { // The match criteria for logs that should be forwarded to this bdtail stream. If empty, all logs // will be forwarded to this stream. matcher.v1.LogMatcher matcher = 2; + + // An optional remote command to execute. Its command_id equals stream_id when present. + DeviceCommandRequest device_command = 3; } message BdTailConfigurations { diff --git a/src/bitdrift_public/protobuf/client/v1/api.proto b/src/bitdrift_public/protobuf/client/v1/api.proto index 71abb9d..ad48b27 100644 --- a/src/bitdrift_public/protobuf/client/v1/api.proto +++ b/src/bitdrift_public/protobuf/client/v1/api.proto @@ -220,6 +220,15 @@ message LogUploadIntentResponse { // A single log upload payload. message LogUploadRequest { + // Identifies a buffer-dump command whose persisted log bytes this upload contributes to. + message DeviceCommandProgress { + // The command ID from the corresponding DeviceCommandRequest. + string command_id = 1 [(validate.rules).string.uuid = true]; + + // The immutable total number of result payload bytes expected from the command. + uint64 total_result_bytes = 2; + } + // A UUID (v4) associated with this upload. This should be generated by the // client. Retries should use the same upload_id. string upload_uuid = 1 [(validate.rules).string = {min_len: 1}]; @@ -257,6 +266,10 @@ message LogUploadRequest { // throughput, low value logs where the client does not need to know whether // the upload succeeded or failed. bool ackless = 4; + + // Command progress metadata for a buffer-dump upload. The outer upload_uuid is its stable + // per-batch idempotency key. + DeviceCommandProgress device_command_progress = 7; } // Empty message to maintain a application layer keep alive mechanism. @@ -308,6 +321,7 @@ message ApiRequest { UploadArtifactIntentRequest artifact_intent = 13; DebugDataRequest debug_data = 14; StateUpdateRequest state_update = 15; + DeviceCommandUpdate device_command_update = 16; } reserved 8; @@ -419,6 +433,10 @@ message UploadArtifactRequest { // Client workflow continuations that should be evaluated when this report is processed. // Only populated for issue-report artifacts; other artifact types must leave this unset. optional workflow.v1.WorkflowReportHandoff workflow_report_handoff = 9; + + // The remote command that produced this artifact. Only command-correlated artifact uploads set + // this field; workflow command artifacts remain session artifacts and leave it unset. + optional string command_id = 10 [(validate.rules).string.uuid = true]; } message UploadArtifactResponse { @@ -668,6 +686,106 @@ message FlushBuffers { workflow.v1.Workflow.Action.ActionFlushBuffers.Streaming streaming = 2; } +// Structured data supplied with a terminal device command result. Artifact bytes are uploaded +// separately through `UploadArtifactRequest` and must not be included in this context. +message DeviceCommandResultContext { + // Command- or transport-supplied fields describing the terminal result. Binary values are + // rejected; artifacts must use `UploadArtifactRequest`. + map fields = 1 [(validate.rules).map.max_pairs = 100]; +} + +// Reports an update from a device command. +message DeviceCommandUpdate { + // Confirms that the device accepted the command. + message Accepted { + // The immutable total number of result payload bytes expected from the command. + optional uint64 total_result_bytes = 1; + } + + // Confirms that the command completed successfully. + message Completed { + // Declares the complete logical attachment produced by the command. The server verifies this + // declaration against its durable attachment catalog before reporting the command as + // succeeded. + message Attachment { + // The command completed without an attachment. + message None {} + + // The command produced one artifact uploaded through UploadArtifactRequest. + message Artifact { + // The stable artifact ID from the acknowledged upload. + string artifact_id = 1 [(validate.rules).string.uuid = true]; + } + + // The command produced log batches uploaded through LogUploadRequest. + message LogBatches { + // The immutable total number of result payload bytes across all acknowledged batches. + uint64 total_result_bytes = 1; + } + + oneof attachment_type { + option (validate.required) = true; + + // The command completed without an attachment. + None none = 1; + + // The command completed with one artifact. + Artifact artifact = 2; + + // The command completed with one logical attachment made from log batches. + LogBatches log_batches = 3; + } + } + + // Whether output was limited before the command completed. + bool output_truncated = 1; + + // Optional structured context supplied with the successful command result. + DeviceCommandResultContext context = 2; + + // The attachment the device completed after uploading. This is optional only for compatibility + // with older clients; new command implementations must always set it. + Attachment attachment = 3; + } + + // Reports that command execution could not be completed. + message Failed { + // Optional structured context supplied with the failed command result. + DeviceCommandResultContext context = 1; + } + + // The command ID from the corresponding DeviceCommandRequest. + string command_id = 1 [(validate.rules).string.uuid = true]; + + // The monotonically increasing sequence number for this command update. + uint64 update_sequence_number = 2; + + oneof update_type { + option (validate.required) = true; + + // Confirms that the device accepted the command. + Accepted accepted = 3; + + // Confirms that the command completed successfully. + Completed completed = 4; + + // Reports that command execution could not be completed. + Failed failed = 5; + } +} + +// Acknowledges that the server persisted a command update. +message DeviceCommandUpdateAck { + // The command ID from the corresponding DeviceCommandRequest. + string command_id = 1 [(validate.rules).string.uuid = true]; + + // The command update sequence number that was persisted. + uint64 update_sequence_number = 2; + + // A non-empty error means the server did not persist the update and the client should retry it. + string error = 3; +} + // The response to Sankey diagram path upload request. message SankeyPathUploadResponse { // The UUID corresponding to the upload request. @@ -758,6 +876,7 @@ message ApiResponse { UploadArtifactResponse artifact_upload = 14; UploadArtifactIntentResponse artifact_intent = 15; StateUpdateResponse state_update = 16; + DeviceCommandUpdateAck device_command_update_ack = 17; } reserved 10; diff --git a/src/bitdrift_public/protobuf/client/v1/artifact.proto b/src/bitdrift_public/protobuf/client/v1/artifact.proto index 10a5bc8..da891b4 100644 --- a/src/bitdrift_public/protobuf/client/v1/artifact.proto +++ b/src/bitdrift_public/protobuf/client/v1/artifact.proto @@ -53,6 +53,9 @@ message ArtifactUploadIndex { // Client workflow continuations that should be evaluated when this report is processed. // Only populated for issue-report artifacts; other artifact types must leave this unset. optional bitdrift_public.protobuf.workflow.v1.WorkflowReportHandoff workflow_report_handoff = 10; + + // The remote command that produced this artifact. This survives local uploader restarts. + optional string command_id = 11; } // List of files, in order of time, that are pending upload. diff --git a/src/bitdrift_public/protobuf/workflow/v1/workflow.proto b/src/bitdrift_public/protobuf/workflow/v1/workflow.proto index 343e358..c8a5c65 100644 --- a/src/bitdrift_public/protobuf/workflow/v1/workflow.proto +++ b/src/bitdrift_public/protobuf/workflow/v1/workflow.proto @@ -13,6 +13,7 @@ import "bitdrift_public/protobuf/matcher/v1/log_matcher.proto"; import "bitdrift_public/protobuf/state/v1/matcher.proto"; import "bitdrift_public/protobuf/state/v1/scope.proto"; import "bitdrift_public/protobuf/workflow/v1/save_field.proto"; +import "bitdrift_public/protobuf/workflow/v1/workflow_command.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -228,16 +229,24 @@ message Workflow { ActionFlushBuffers action_flush_buffers = 1; ActionEmitMetric action_emit_metric = 2; ActionEmitSankeyDiagram action_emit_sankey_diagram = 3; - ActionTakeScreenshot action_take_screenshot = 4; ActionGenerateLog action_generate_log = 5; ActionStartTracing action_start_tracing = 6; + ActionRunCommand action_run_command = 7; } + reserved 4; + reserved "action_take_screenshot"; + // Specifies that the current session will start trace sampling. Tracing will stop when the // workflow run ends. The exception to this is if the run results in a flush. If this happens, // tracing will stay on until flush streaming completes. message ActionStartTracing {} + // Runs one locally registered or built-in command when this transition is taken. + message ActionRunCommand { + WorkflowCommandSelector command_selector = 1 [(validate.rules).message = {required: true}]; + } + // Generates a new log message. This log will be injected into the workflow engine and processed // like any other log, either by this workflow or by another workflow. message ActionGenerateLog { @@ -468,11 +477,6 @@ message Workflow { reserved 5; reserved "feature_flag_extracted"; } - - // Emit a log containing application screenshot. - message ActionTakeScreenshot { - reserved 1; - } } message Execution { diff --git a/src/bitdrift_public/protobuf/workflow/v1/workflow_command.proto b/src/bitdrift_public/protobuf/workflow/v1/workflow_command.proto new file mode 100644 index 0000000..5f96a8e --- /dev/null +++ b/src/bitdrift_public/protobuf/workflow/v1/workflow_command.proto @@ -0,0 +1,37 @@ +// api - bitdrift's client/server API definitions +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code and APIs are governed by a source available license that can be found in +// the LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +syntax = "proto3"; + +package bitdrift_public.protobuf.workflow.v1; + +import "validate/validate.proto"; + +// Selects one locally registered or built-in workflow command. +message WorkflowCommandSelector { + // Built-in commands supported directly by the client workflow runtime. + message BuiltinCommand { + oneof command_type { + option (validate.required) = true; + + // Captures one application screenshot using the platform's configured defaults. + TakeScreenshot take_screenshot = 1; + } + + message TakeScreenshot {} + } + + oneof command_selector { + option (validate.required) = true; + + // An opaque customer-registered command handler identifier. + string registered_command_id = 1 [(validate.rules).string.uuid = true]; + + // A strongly typed command implemented by the client workflow runtime. + BuiltinCommand builtin_command = 2; + } +}