// SPDX-License-Identifier: MIT // Internal wire format between the host process of ermite-runner and the // executor running inside a sandbox (vsock in a micro-VM, Unix socket in a // container). Frames are length-prefixed on the stream (see frame.go); this file // defines the payload. It is internal to ermite-runner: no compatibility is // promised between runner versions, which is safe because the host attaches its // own executable to every sandbox. syntax = "proto3"; package ermite.runner.exec.v1; option go_package = "bnema.dev/ermite-runner/internal/execstream/execpb"; // Frame is the only message on the stream. message Frame { oneof body { Hello hello = 1; // executor -> host, first frame Run run = 2; // host -> executor, after Hello Output output = 3; // executor -> host Exit exit = 4; // executor -> host, last frame Cancel cancel = 5; // host -> executor StepStarted step_started = 6; // executor -> host StepCompleted step_completed = 7; // executor -> host Log log = 8; // executor -> host JobResult job_result = 9; // executor -> host, before Exit StepAnnotations step_annotations = 10; // executor -> host, before the StepCompleted of the step StepSummary step_summary = 11; // executor -> host, before the StepCompleted of the step ResolveRequest resolve_request = 12; // executor -> host ResolveResponse resolve_response = 13; // host -> executor, answers a ResolveRequest NamedValue secret = 14; // host -> executor, one per secret, before Run NamedValue var = 15; // host -> executor, one per variable, before Run } } // NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job. // They travel in frames of their own, before Run, rather than in JobSpec: a // frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of // 48 KiB, so a map in Run could not carry them. The executor keeps them until // Run arrives. Secrets are never logged nor written to disk. message NamedValue { string name = 1; string value = 2; } message Hello { string version = 1; // runner version of the executor string sandbox = 2; // "vm" or "container" string hostname = 3; map boot_ms = 4; } // Run asks the executor to run a script or a job. Without job it is the stub // executor of sandbox-check: the script is run with `sh -c`; an empty script // means the built-in diagnostics script. With job the executor runs the job // like a GitHub Actions runner and reports it with StepStarted, Log, // StepCompleted and JobResult frames, then Exit. message Run { string script = 1; map env = 2; JobSpec job = 3; } // JobSpec is the job to run: the fields of the job that ermite handed to the // host process, minus the tokens (the sandbox never sees them). message JobSpec { string workflow_path = 1; string workflow_yaml = 2; string job_key = 3; string matrix = 4; // JSON object string contexts = 5; // JSON object string repo = 6; // repository name int32 timeout_minutes = 7; string runner_name = 8; string runner_version = 9; // runner_environment is the runner.environment value ("github-hosted" or "self-hosted"). string runner_environment = 10; bool debug = 11; // ACTIONS_STEP_DEBUG // deadline_ms is what is left of the job timeout when the host sends this // Run, in milliseconds: the timeout counts from the moment the host received // the job (runner.proto: Job.timeout_minutes), not from the start of the // executor, and booting the sandbox takes time. 0 means "use // timeout_minutes from now". int64 deadline_ms = 12; // send_annotations and send_summaries say that the server keeps annotations // (feature ci.annotations) and step summaries (ci.summaries). The executor // sends StepAnnotations and StepSummary frames only when they are set. bool send_annotations = 13; bool send_summaries = 14; // cache_port and cache_token, when set, are the runner's cache service for // this job. The service is not on the network: the executor starts a relay // on the sandbox's own loopback, 127.0.0.1:, that carries each // connection over the host channel (a vsock port, or a Unix socket next to // the executor's) to the service. The executor gives // ACTIONS_CACHE_URL=http://127.0.0.1:/ and the token as // ACTIONS_RUNTIME_TOKEN to node and docker actions only, as the official // runner does, and masks the token. The token is not an ermite credential: it // is a JWT (HS256) that only the runner's runtime services accept, scoped to // this job's repository and refs (and its run, see results). uint32 cache_port = 15; string cache_token = 16; // Job.secrets and Job.vars are not here: they come in NamedValue frames // before Run (see NamedValue). The executor takes the vars of those frames // for the "vars" context when contexts has none. reserved 17, 18; reserved "secrets", "vars"; // secrets_withheld is set by the host when the server sends Job.trusted and // the job is not trusted (feature ci.secrets): "Set up job" then says that // secrets are not available. Never set for servers without the feature, for // which an absent Job.trusted only means "not sent". bool secrets_withheld = 19; // id_token_url and id_token_request_token are Job.id_token_url and // Job.id_token_request_token: the OIDC token endpoint of the server, its // origin rewritten as for github.server_url, and the bearer for it. The // executor gives them as ACTIONS_ID_TOKEN_REQUEST_URL and // ACTIONS_ID_TOKEN_REQUEST_TOKEN to run steps, node actions and docker // actions (ScriptHandler.cs:315-319, NodeScriptActionHandler.cs:66-70, // ContainerActionHandler.cs:232-236), and masks the token before anything // runs. Both are set or neither. (21 and 22 are taken by the artifacts // branch.) string id_token_url = 23; string id_token_request_token = 24; // token_permissions are the effective permissions of the job's GITHUB_TOKEN // as the server sent them (Job.permissions, feature ci.secrets): "Set up job" // prints them instead of working them out from the workflow. Unset when the // server sent none (an older server): the executor then reads them from the // workflow. TokenPermissions token_permissions = 20; // cache_mode is ACTIONS_CACHE_MODE of the node and docker actions: "write" // when the job's cache scope has a write ref, "read" when it has none. Unset // without a cache service, and unset when the channel serves the results // service only (results set, no cache scope): no ACTIONS_CACHE_URL then. string cache_mode = 21; // results says that the service behind cache_port also serves artifacts // (Twirp github.actions.results.api.v1.ArtifactService and its blobs) and // that cache_token carries the job's Actions.Results scope. The executor // then gives node and docker actions ACTIONS_RESULTS_URL (the same origin as // ACTIONS_CACHE_URL) and sets GITHUB_SERVER_URL to a *.ghe.localhost host in // the process of the node steps of the artifact actions only (README, // "Artifacts"). Set only when the server has the feature ci.artifacts. bool results = 22; } // TokenPermissions maps a permission key ("contents", "pull-requests", ...) to // "read", "write" or "none"; a key that is absent is none. message TokenPermissions { map levels = 1; } enum StreamKind { STREAM_KIND_UNSPECIFIED = 0; STREAM_KIND_STDOUT = 1; STREAM_KIND_STDERR = 2; } message Output { StreamKind stream = 1; bytes data = 2; int64 unix_nano = 3; } message Exit { int32 code = 1; string error = 2; // set when the executor failed to run the script at all } // Cancel asks the executor to stop the running script. The executor escalates // like the official runner: SIGINT to the script's process group at once, // SIGTERM 7.5 s later, SIGKILL 2.5 s after that. message Cancel { // reason is for the job log and diagnostics ("cancelled by the user", // "job timeout", ...); it never changes the behaviour. string reason = 1; // kind tells the job executor how the job ends: cancelled, timed out, or // cancelled because the runner shuts down. The stub executor ignores it. CancelKind kind = 2; } enum CancelKind { CANCEL_KIND_UNSPECIFIED = 0; // same as USER CANCEL_KIND_USER = 1; CANCEL_KIND_TIMEOUT = 2; CANCEL_KIND_SHUTDOWN = 3; } // Steps are numbered like GitHub's log: "Set up job" is 1, the steps of the // workflow follow, "Complete job" is last. message StepStarted { int32 number = 1; string name = 2; int64 unix_nano = 3; } message StepCompleted { int32 number = 1; string name = 2; StepConclusion conclusion = 3; int64 unix_nano = 4; // outcome is steps..outcome: the result before continue-on-error. It // differs from conclusion only for a failed step with continue-on-error. // UNSPECIFIED means the same as conclusion. StepConclusion outcome = 5; } enum AnnotationLevel { ANNOTATION_LEVEL_UNSPECIFIED = 0; ANNOTATION_LEVEL_NOTICE = 1; ANNOTATION_LEVEL_WARNING = 2; ANNOTATION_LEVEL_FAILURE = 3; } // Annotation is one ::notice, ::warning or ::error (or a problem matcher // match), masked. The host validates every field again before it sends them. message Annotation { AnnotationLevel level = 1; string title = 2; string message = 3; string file = 4; // relative to the repository when the file is in it int32 start_line = 5; int32 end_line = 6; int32 start_column = 7; int32 end_column = 8; } // StepAnnotations is the complete list of the annotations of a step (the ones // the runner keeps: at most 10 per level), replacing any earlier list of that // step. Annotations of the steps inside a composite action are those of the // step of the workflow that uses it. message StepAnnotations { int32 step = 1; repeated Annotation annotations = 2; } // StepSummary is the masked content of GITHUB_STEP_SUMMARY after a step, at // most 1 MiB. message StepSummary { int32 step = 1; string markdown = 2; } enum StepConclusion { STEP_CONCLUSION_UNSPECIFIED = 0; STEP_CONCLUSION_SUCCESS = 1; STEP_CONCLUSION_FAILURE = 2; STEP_CONCLUSION_CANCELLED = 3; STEP_CONCLUSION_SKIPPED = 4; STEP_CONCLUSION_TIMED_OUT = 5; // jobs only } // Log is one line of the job log, without terminator, valid UTF-8. A line of // more than 64 KiB is cut by the executor into pieces at character boundaries; // every piece but the first has continued set (the AppendLog rule). message Log { int32 step = 1; int64 unix_nano = 2; string text = 3; bool continued = 4; } // JobResult is the outcome of the job; Exit follows. message JobResult { StepConclusion conclusion = 1; map outputs = 2; string error = 3; bool runner_shutdown = 4; } // ResolveRequest asks the host to resolve actions with RunnerService. // ResolveActions: the job token stays in the host, so the executor cannot call // it. The executor sends one per level of composite actions (the runner's own // "one more round per level") and waits for the ResolveResponse with the same id. message ResolveRequest { int64 id = 1; repeated string uses = 2; // "owner/repo[/path]@ref", at most 100 } // ResolveResponse answers a ResolveRequest. error is set when the call itself // failed (the executor fails the job); actions has one entry per uses otherwise. message ResolveResponse { int64 id = 1; string error = 2; repeated ResolvedAction actions = 3; } // ResolvedAction mirrors ermite.ResolvedAction (the token is a secret: the // executor masks it). message ResolvedAction { string ref = 1; string name_with_owner = 2; string path = 3; string resolved_sha = 4; string tarball_url = 5; string token = 6; string source = 7; string error = 8; // auth_origin (scheme://host[:port], as the sandbox reaches it) is the only // origin the token may be sent to; empty means the token is never sent. The // host sets it for archives served by the ermite server, from its own // configuration, never from the URL alone. string auth_origin = 9; }