From ef862ab1d31de37b52d1744fa52f760a93505d97 Mon Sep 17 00:00:00 2001 From: mintaka Date: Thu, 27 Aug 2026 21:48:07 -0400 Subject: [PATCH 1/2] feat(guest_control): grow GuestControl with the V2b exec surface (RIG-2493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U1 of the frozen microVM Runner V2b plan (`docs/designs/infra/runtime/compass-elastic-session-runtime/microvm-v2b-guest-supervisor-exec.md`, §Plan U1). Grows `proto/compass/v1/guest_control.proto` additively per §(a) with the four exec-surface RPCs and their messages, plus one additive field on the V2a `HealthResponse`, and regenerates the internal-go lane. ### What - `Exec(ExecRequest) returns (ExecResponse)` — one-shot; a non-zero exit rides `ExecResponse.exit_code`, never a Connect error (the wire mirror of `podman.go:310-313`). - `ExecStream(stream ExecStreamRequest) returns (stream ExecStreamResponse)` — bidi; direction-specific `oneof` frames (`StartExec|stdin|StdinClose` up, `ExecStarted|stdout|stderr|ExecExit` down). Named `ExecStreamRequest`/`ExecStreamResponse` rather than the parent's shared `ExecStreamFrame` sketch, forced by buf's RPC_REQUEST/RESPONSE_STANDARD_NAME (OQ-B, resolved in-record). - `Signal(SignalRequest) returns (SignalResponse)` — kill an exec by `exec_id`; empty `exec_id` targets the guest (Stop, §(d)). - `Provision(ProvisionRequest) returns (ProvisionResponse)` — the `ready -> provisioned` gate transition; carries `default_exec_uid` + `base_env` now, the `nft_script` branch is V3's. - `HealthResponse.boot_nonce` (field 4, additive) — the host-minted cmdline nonce guestd echoes so Start binds the guest to the VM it launched (§(e), OQ-A resolved). - Service-header auth paragraph rewritten to record the (e) structural-identity resolution. Additive-only (new RPCs, new messages, one new field): `buf breaking` is satisfied by construction, and the file stays on the internal-only lane (`go/internal/gen`, no TS, no public `go/gen`). ### Guard for the stacked follow-ups The generated `GuestControlHandler` interface widened to all five RPCs, so the V2a Health-only `healthService` no longer satisfies it. This commit embeds `UnimplementedGuestControlHandler` in `healthService` so the four new RPCs answer `CodeUnimplemented` and the V2a Health path is byte-unchanged; U2 replaces `healthService` with the real supervisor. ### Verification `moon run compass-proto:ci` green (gen-fence, drift, lint, breaking); `go build ./internal/gen/... ./internal/guestd/... ./internal/runtime/...` and `go vet ./internal/guestd/...` clean. Ledger-impact: none Spec-impact: none Refs RIG-2493 Co-authored-by: Matt Wilkinson --- .../guest_control.connect.go | 150 ++- .../gen/compass/v1/guest_control.pb.go | 924 +++++++++++++++++- go/internal/guestd/health.go | 14 +- go/internal/runtime/microvm/dial_test.go | 6 +- proto/compass/v1/guest_control.proto | 145 ++- 5 files changed, 1210 insertions(+), 29 deletions(-) diff --git a/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go b/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go index dcaf02f19..ec1768adf 100644 --- a/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go +++ b/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go @@ -64,11 +64,41 @@ const ( const ( // GuestControlHealthProcedure is the fully-qualified name of the GuestControl's Health RPC. GuestControlHealthProcedure = "/compass.v1.GuestControl/Health" + // GuestControlExecProcedure is the fully-qualified name of the GuestControl's Exec RPC. + GuestControlExecProcedure = "/compass.v1.GuestControl/Exec" + // GuestControlExecStreamProcedure is the fully-qualified name of the GuestControl's ExecStream RPC. + GuestControlExecStreamProcedure = "/compass.v1.GuestControl/ExecStream" + // GuestControlSignalProcedure is the fully-qualified name of the GuestControl's Signal RPC. + GuestControlSignalProcedure = "/compass.v1.GuestControl/Signal" + // GuestControlProvisionProcedure is the fully-qualified name of the GuestControl's Provision RPC. + GuestControlProvisionProcedure = "/compass.v1.GuestControl/Provision" ) // GuestControlClient is a client for the compass.v1.GuestControl service. type GuestControlClient interface { Health(context.Context, *connect.Request[v1.HealthRequest]) (*connect.Response[v1.HealthResponse], error) + // Exec runs one command to completion and returns its captured output. A + // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is + // non-zero — never a Connect error. A Connect error means the exec could not + // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an + // unprovisioned gate. + Exec(context.Context, *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) + // ExecStream runs a long-lived command over one bidi stream: the request + // stream carries the start frame, then stdin bytes and an optional + // half-close; the response stream carries the exec_id, then interleaved + // stdout/stderr, then exactly one terminal exit frame. Kill rides Signal + // (below), not this stream, so a teardown never contends with an in-flight + // stdin write. + ExecStream(context.Context) *connect.BidiStreamForClient[v1.ExecStreamRequest, v1.ExecStreamResponse] + // Signal delivers a signal to a running exec by exec_id; an empty exec_id + // targets the guest itself (graceful Stop, record §(d)). Signalling an + // already-exited exec is a no-op success. + Signal(context.Context, *connect.Request[v1.SignalRequest]) (*connect.Response[v1.SignalResponse], error) + // Provision transitions the supervisor ready -> provisioned: it records the + // session's default exec uid and base env and, when nft_script is non-empty, + // arms egress (V3; a non-empty script is unimplemented in V2b). Exec and + // ExecStream are refused until Provision succeeds. + Provision(context.Context, *connect.Request[v1.ProvisionRequest]) (*connect.Response[v1.ProvisionResponse], error) } // NewGuestControlClient constructs a client for the compass.v1.GuestControl service. By default, it @@ -88,12 +118,40 @@ func NewGuestControlClient(httpClient connect.HTTPClient, baseURL string, opts . connect.WithSchema(guestControlMethods.ByName("Health")), connect.WithClientOptions(opts...), ), + exec: connect.NewClient[v1.ExecRequest, v1.ExecResponse]( + httpClient, + baseURL+GuestControlExecProcedure, + connect.WithSchema(guestControlMethods.ByName("Exec")), + connect.WithClientOptions(opts...), + ), + execStream: connect.NewClient[v1.ExecStreamRequest, v1.ExecStreamResponse]( + httpClient, + baseURL+GuestControlExecStreamProcedure, + connect.WithSchema(guestControlMethods.ByName("ExecStream")), + connect.WithClientOptions(opts...), + ), + signal: connect.NewClient[v1.SignalRequest, v1.SignalResponse]( + httpClient, + baseURL+GuestControlSignalProcedure, + connect.WithSchema(guestControlMethods.ByName("Signal")), + connect.WithClientOptions(opts...), + ), + provision: connect.NewClient[v1.ProvisionRequest, v1.ProvisionResponse]( + httpClient, + baseURL+GuestControlProvisionProcedure, + connect.WithSchema(guestControlMethods.ByName("Provision")), + connect.WithClientOptions(opts...), + ), } } // guestControlClient implements GuestControlClient. type guestControlClient struct { - health *connect.Client[v1.HealthRequest, v1.HealthResponse] + health *connect.Client[v1.HealthRequest, v1.HealthResponse] + exec *connect.Client[v1.ExecRequest, v1.ExecResponse] + execStream *connect.Client[v1.ExecStreamRequest, v1.ExecStreamResponse] + signal *connect.Client[v1.SignalRequest, v1.SignalResponse] + provision *connect.Client[v1.ProvisionRequest, v1.ProvisionResponse] } // Health calls compass.v1.GuestControl.Health. @@ -101,9 +159,51 @@ func (c *guestControlClient) Health(ctx context.Context, req *connect.Request[v1 return c.health.CallUnary(ctx, req) } +// Exec calls compass.v1.GuestControl.Exec. +func (c *guestControlClient) Exec(ctx context.Context, req *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) { + return c.exec.CallUnary(ctx, req) +} + +// ExecStream calls compass.v1.GuestControl.ExecStream. +func (c *guestControlClient) ExecStream(ctx context.Context) *connect.BidiStreamForClient[v1.ExecStreamRequest, v1.ExecStreamResponse] { + return c.execStream.CallBidiStream(ctx) +} + +// Signal calls compass.v1.GuestControl.Signal. +func (c *guestControlClient) Signal(ctx context.Context, req *connect.Request[v1.SignalRequest]) (*connect.Response[v1.SignalResponse], error) { + return c.signal.CallUnary(ctx, req) +} + +// Provision calls compass.v1.GuestControl.Provision. +func (c *guestControlClient) Provision(ctx context.Context, req *connect.Request[v1.ProvisionRequest]) (*connect.Response[v1.ProvisionResponse], error) { + return c.provision.CallUnary(ctx, req) +} + // GuestControlHandler is an implementation of the compass.v1.GuestControl service. type GuestControlHandler interface { Health(context.Context, *connect.Request[v1.HealthRequest]) (*connect.Response[v1.HealthResponse], error) + // Exec runs one command to completion and returns its captured output. A + // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is + // non-zero — never a Connect error. A Connect error means the exec could not + // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an + // unprovisioned gate. + Exec(context.Context, *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) + // ExecStream runs a long-lived command over one bidi stream: the request + // stream carries the start frame, then stdin bytes and an optional + // half-close; the response stream carries the exec_id, then interleaved + // stdout/stderr, then exactly one terminal exit frame. Kill rides Signal + // (below), not this stream, so a teardown never contends with an in-flight + // stdin write. + ExecStream(context.Context, *connect.BidiStream[v1.ExecStreamRequest, v1.ExecStreamResponse]) error + // Signal delivers a signal to a running exec by exec_id; an empty exec_id + // targets the guest itself (graceful Stop, record §(d)). Signalling an + // already-exited exec is a no-op success. + Signal(context.Context, *connect.Request[v1.SignalRequest]) (*connect.Response[v1.SignalResponse], error) + // Provision transitions the supervisor ready -> provisioned: it records the + // session's default exec uid and base env and, when nft_script is non-empty, + // arms egress (V3; a non-empty script is unimplemented in V2b). Exec and + // ExecStream are refused until Provision succeeds. + Provision(context.Context, *connect.Request[v1.ProvisionRequest]) (*connect.Response[v1.ProvisionResponse], error) } // NewGuestControlHandler builds an HTTP handler from the service implementation. It returns the @@ -119,10 +219,42 @@ func NewGuestControlHandler(svc GuestControlHandler, opts ...connect.HandlerOpti connect.WithSchema(guestControlMethods.ByName("Health")), connect.WithHandlerOptions(opts...), ) + guestControlExecHandler := connect.NewUnaryHandler( + GuestControlExecProcedure, + svc.Exec, + connect.WithSchema(guestControlMethods.ByName("Exec")), + connect.WithHandlerOptions(opts...), + ) + guestControlExecStreamHandler := connect.NewBidiStreamHandler( + GuestControlExecStreamProcedure, + svc.ExecStream, + connect.WithSchema(guestControlMethods.ByName("ExecStream")), + connect.WithHandlerOptions(opts...), + ) + guestControlSignalHandler := connect.NewUnaryHandler( + GuestControlSignalProcedure, + svc.Signal, + connect.WithSchema(guestControlMethods.ByName("Signal")), + connect.WithHandlerOptions(opts...), + ) + guestControlProvisionHandler := connect.NewUnaryHandler( + GuestControlProvisionProcedure, + svc.Provision, + connect.WithSchema(guestControlMethods.ByName("Provision")), + connect.WithHandlerOptions(opts...), + ) return "/compass.v1.GuestControl/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case GuestControlHealthProcedure: guestControlHealthHandler.ServeHTTP(w, r) + case GuestControlExecProcedure: + guestControlExecHandler.ServeHTTP(w, r) + case GuestControlExecStreamProcedure: + guestControlExecStreamHandler.ServeHTTP(w, r) + case GuestControlSignalProcedure: + guestControlSignalHandler.ServeHTTP(w, r) + case GuestControlProvisionProcedure: + guestControlProvisionHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -135,3 +267,19 @@ type UnimplementedGuestControlHandler struct{} func (UnimplementedGuestControlHandler) Health(context.Context, *connect.Request[v1.HealthRequest]) (*connect.Response[v1.HealthResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.GuestControl.Health is not implemented")) } + +func (UnimplementedGuestControlHandler) Exec(context.Context, *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.GuestControl.Exec is not implemented")) +} + +func (UnimplementedGuestControlHandler) ExecStream(context.Context, *connect.BidiStream[v1.ExecStreamRequest, v1.ExecStreamResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.GuestControl.ExecStream is not implemented")) +} + +func (UnimplementedGuestControlHandler) Signal(context.Context, *connect.Request[v1.SignalRequest]) (*connect.Response[v1.SignalResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.GuestControl.Signal is not implemented")) +} + +func (UnimplementedGuestControlHandler) Provision(context.Context, *connect.Request[v1.ProvisionRequest]) (*connect.Response[v1.ProvisionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.GuestControl.Provision is not implemented")) +} diff --git a/go/internal/gen/compass/v1/guest_control.pb.go b/go/internal/gen/compass/v1/guest_control.pb.go index 2d6bbf54c..1fdee261b 100644 --- a/go/internal/gen/compass/v1/guest_control.pb.go +++ b/go/internal/gen/compass/v1/guest_control.pb.go @@ -91,8 +91,12 @@ type HealthResponse struct { GuestdVersion string `protobuf:"bytes,1,opt,name=guestd_version,json=guestdVersion,proto3" json:"guestd_version,omitempty"` NetProvisioned bool `protobuf:"varint,2,opt,name=net_provisioned,json=netProvisioned,proto3" json:"net_provisioned,omitempty"` WorkspaceMounted bool `protobuf:"varint,3,opt,name=workspace_mounted,json=workspaceMounted,proto3" json:"workspace_mounted,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // boot_nonce echoes the host-minted compass.boot_nonce cmdline value so Start + // can bind this guest to the VM it launched before opening the exec gate + // (record §(e), OQ-A). Empty until V2b guestd parses the cmdline nonce. + BootNonce []byte `protobuf:"bytes,4,opt,name=boot_nonce,json=bootNonce,proto3" json:"boot_nonce,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HealthResponse) Reset() { @@ -146,19 +150,864 @@ func (x *HealthResponse) GetWorkspaceMounted() bool { return false } +func (x *HealthResponse) GetBootNonce() []byte { + if x != nil { + return x.BootNonce + } + return nil +} + +// ExecRequest is a one-shot exec. It mirrors runtime.ExecSpec field for field. +type ExecRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // command is argv; command[0] is the program. + Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"` + // uid is the exec's user. Absent = the session default uid set by Provision + // (the baked agent uid). uid 0 is REFUSED with a failed-precondition error + // before any spawn — the guest supervisor never runs an exec as root. + Uid *uint32 `protobuf:"varint,2,opt,name=uid,proto3,oneof" json:"uid,omitempty"` + // workdir is the working directory; absent = the child's default. + Workdir *string `protobuf:"bytes,3,opt,name=workdir,proto3,oneof" json:"workdir,omitempty"` + // env is merged over the session base env; exec-specific keys win. + Env map[string]string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // stdin is fed to the child's stdin — the body over the wire, NEVER the argv, + // so a script and any secret it embeds never appear in the guest process + // list. + Stdin []byte `protobuf:"bytes,5,opt,name=stdin,proto3,oneof" json:"stdin,omitempty"` + // timeout_seconds bounds the command; the guest enforces it too. + TimeoutSeconds uint32 `protobuf:"varint,6,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecRequest) Reset() { + *x = ExecRequest{} + mi := &file_compass_v1_guest_control_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecRequest) ProtoMessage() {} + +func (x *ExecRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecRequest.ProtoReflect.Descriptor instead. +func (*ExecRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{2} +} + +func (x *ExecRequest) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ExecRequest) GetUid() uint32 { + if x != nil && x.Uid != nil { + return *x.Uid + } + return 0 +} + +func (x *ExecRequest) GetWorkdir() string { + if x != nil && x.Workdir != nil { + return *x.Workdir + } + return "" +} + +func (x *ExecRequest) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +func (x *ExecRequest) GetStdin() []byte { + if x != nil { + return x.Stdin + } + return nil +} + +func (x *ExecRequest) GetTimeoutSeconds() uint32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +type ExecResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` + Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"` + // exit_code is the command's exit status. Non-zero is a successful call with + // a failed command, never a Connect error (see the Exec doc-comment). + ExitCode int32 `protobuf:"varint,3,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecResponse) Reset() { + *x = ExecResponse{} + mi := &file_compass_v1_guest_control_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecResponse) ProtoMessage() {} + +func (x *ExecResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecResponse.ProtoReflect.Descriptor instead. +func (*ExecResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{3} +} + +func (x *ExecResponse) GetStdout() []byte { + if x != nil { + return x.Stdout + } + return nil +} + +func (x *ExecResponse) GetStderr() []byte { + if x != nil { + return x.Stderr + } + return nil +} + +func (x *ExecResponse) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +// ExecStreamRequest frames flow host -> guest: exactly one StartExec first, +// then any number of stdin frames, then an optional StdinClose half-close. +type ExecStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Frame: + // + // *ExecStreamRequest_Start + // *ExecStreamRequest_Stdin + // *ExecStreamRequest_StdinClose + Frame isExecStreamRequest_Frame `protobuf_oneof:"frame"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecStreamRequest) Reset() { + *x = ExecStreamRequest{} + mi := &file_compass_v1_guest_control_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecStreamRequest) ProtoMessage() {} + +func (x *ExecStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecStreamRequest.ProtoReflect.Descriptor instead. +func (*ExecStreamRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{4} +} + +func (x *ExecStreamRequest) GetFrame() isExecStreamRequest_Frame { + if x != nil { + return x.Frame + } + return nil +} + +func (x *ExecStreamRequest) GetStart() *StartExec { + if x != nil { + if x, ok := x.Frame.(*ExecStreamRequest_Start); ok { + return x.Start + } + } + return nil +} + +func (x *ExecStreamRequest) GetStdin() []byte { + if x != nil { + if x, ok := x.Frame.(*ExecStreamRequest_Stdin); ok { + return x.Stdin + } + } + return nil +} + +func (x *ExecStreamRequest) GetStdinClose() *StdinClose { + if x != nil { + if x, ok := x.Frame.(*ExecStreamRequest_StdinClose); ok { + return x.StdinClose + } + } + return nil +} + +type isExecStreamRequest_Frame interface { + isExecStreamRequest_Frame() +} + +type ExecStreamRequest_Start struct { + Start *StartExec `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +} + +type ExecStreamRequest_Stdin struct { + Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` +} + +type ExecStreamRequest_StdinClose struct { + StdinClose *StdinClose `protobuf:"bytes,3,opt,name=stdin_close,json=stdinClose,proto3,oneof"` +} + +func (*ExecStreamRequest_Start) isExecStreamRequest_Frame() {} + +func (*ExecStreamRequest_Stdin) isExecStreamRequest_Frame() {} + +func (*ExecStreamRequest_StdinClose) isExecStreamRequest_Frame() {} + +type StartExec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"` + // uid as in ExecRequest: absent = session default; 0 REFUSED. + Uid *uint32 `protobuf:"varint,2,opt,name=uid,proto3,oneof" json:"uid,omitempty"` + Workdir *string `protobuf:"bytes,3,opt,name=workdir,proto3,oneof" json:"workdir,omitempty"` + Env map[string]string `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartExec) Reset() { + *x = StartExec{} + mi := &file_compass_v1_guest_control_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartExec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartExec) ProtoMessage() {} + +func (x *StartExec) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartExec.ProtoReflect.Descriptor instead. +func (*StartExec) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{5} +} + +func (x *StartExec) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *StartExec) GetUid() uint32 { + if x != nil && x.Uid != nil { + return *x.Uid + } + return 0 +} + +func (x *StartExec) GetWorkdir() string { + if x != nil && x.Workdir != nil { + return *x.Workdir + } + return "" +} + +func (x *StartExec) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +// StdinClose half-closes the child's stdin pipe without ending the stream. +type StdinClose struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StdinClose) Reset() { + *x = StdinClose{} + mi := &file_compass_v1_guest_control_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StdinClose) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StdinClose) ProtoMessage() {} + +func (x *StdinClose) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StdinClose.ProtoReflect.Descriptor instead. +func (*StdinClose) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{6} +} + +// ExecStreamResponse frames flow guest -> host: exactly one ExecStarted first, +// then interleaved stdout/stderr, then exactly one ExecExit. +type ExecStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Frame: + // + // *ExecStreamResponse_Started + // *ExecStreamResponse_Stdout + // *ExecStreamResponse_Stderr + // *ExecStreamResponse_Exit + Frame isExecStreamResponse_Frame `protobuf_oneof:"frame"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecStreamResponse) Reset() { + *x = ExecStreamResponse{} + mi := &file_compass_v1_guest_control_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecStreamResponse) ProtoMessage() {} + +func (x *ExecStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecStreamResponse.ProtoReflect.Descriptor instead. +func (*ExecStreamResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{7} +} + +func (x *ExecStreamResponse) GetFrame() isExecStreamResponse_Frame { + if x != nil { + return x.Frame + } + return nil +} + +func (x *ExecStreamResponse) GetStarted() *ExecStarted { + if x != nil { + if x, ok := x.Frame.(*ExecStreamResponse_Started); ok { + return x.Started + } + } + return nil +} + +func (x *ExecStreamResponse) GetStdout() []byte { + if x != nil { + if x, ok := x.Frame.(*ExecStreamResponse_Stdout); ok { + return x.Stdout + } + } + return nil +} + +func (x *ExecStreamResponse) GetStderr() []byte { + if x != nil { + if x, ok := x.Frame.(*ExecStreamResponse_Stderr); ok { + return x.Stderr + } + } + return nil +} + +func (x *ExecStreamResponse) GetExit() *ExecExit { + if x != nil { + if x, ok := x.Frame.(*ExecStreamResponse_Exit); ok { + return x.Exit + } + } + return nil +} + +type isExecStreamResponse_Frame interface { + isExecStreamResponse_Frame() +} + +type ExecStreamResponse_Started struct { + Started *ExecStarted `protobuf:"bytes,1,opt,name=started,proto3,oneof"` +} + +type ExecStreamResponse_Stdout struct { + Stdout []byte `protobuf:"bytes,2,opt,name=stdout,proto3,oneof"` +} + +type ExecStreamResponse_Stderr struct { + Stderr []byte `protobuf:"bytes,3,opt,name=stderr,proto3,oneof"` +} + +type ExecStreamResponse_Exit struct { + Exit *ExecExit `protobuf:"bytes,4,opt,name=exit,proto3,oneof"` +} + +func (*ExecStreamResponse_Started) isExecStreamResponse_Frame() {} + +func (*ExecStreamResponse_Stdout) isExecStreamResponse_Frame() {} + +func (*ExecStreamResponse_Stderr) isExecStreamResponse_Frame() {} + +func (*ExecStreamResponse_Exit) isExecStreamResponse_Frame() {} + +// ExecStarted is the first response frame; exec_id is the handle Signal targets. +type ExecStarted struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecStarted) Reset() { + *x = ExecStarted{} + mi := &file_compass_v1_guest_control_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecStarted) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecStarted) ProtoMessage() {} + +func (x *ExecStarted) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecStarted.ProtoReflect.Descriptor instead. +func (*ExecStarted) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{8} +} + +func (x *ExecStarted) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +// ExecExit is the terminal response frame, emitted from guestd's own reap. +type ExecExit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // exit_code is meaningful when signal == 0. + ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + // signal is non-zero when the child died by signal (e.g. SIGKILL on Kill). + Signal int32 `protobuf:"varint,2,opt,name=signal,proto3" json:"signal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecExit) Reset() { + *x = ExecExit{} + mi := &file_compass_v1_guest_control_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecExit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecExit) ProtoMessage() {} + +func (x *ExecExit) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecExit.ProtoReflect.Descriptor instead. +func (*ExecExit) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{9} +} + +func (x *ExecExit) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +func (x *ExecExit) GetSignal() int32 { + if x != nil { + return x.Signal + } + return 0 +} + +type SignalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // exec_id targets a running exec; "" targets the guest itself (Stop). + ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + // signal is the signal number: SIGKILL for a Kill, SIGTERM for Stop. + Signal int32 `protobuf:"varint,2,opt,name=signal,proto3" json:"signal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalRequest) Reset() { + *x = SignalRequest{} + mi := &file_compass_v1_guest_control_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalRequest) ProtoMessage() {} + +func (x *SignalRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignalRequest.ProtoReflect.Descriptor instead. +func (*SignalRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{10} +} + +func (x *SignalRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *SignalRequest) GetSignal() int32 { + if x != nil { + return x.Signal + } + return 0 +} + +type SignalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalResponse) Reset() { + *x = SignalResponse{} + mi := &file_compass_v1_guest_control_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalResponse) ProtoMessage() {} + +func (x *SignalResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignalResponse.ProtoReflect.Descriptor instead. +func (*SignalResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{11} +} + +type ProvisionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // nft_script is the egress ruleset (V3, EgressPolicy.NftScript()); empty in + // V2b, where a non-empty value is unimplemented. + NftScript string `protobuf:"bytes,1,opt,name=nft_script,json=nftScript,proto3" json:"nft_script,omitempty"` + // default_exec_uid is the session's agent uid (ContainerSpec.UID), the + // default for an exec with no uid. Validated non-zero. + DefaultExecUid uint32 `protobuf:"varint,2,opt,name=default_exec_uid,json=defaultExecUid,proto3" json:"default_exec_uid,omitempty"` + // base_env is the base environment every exec inherits (ContainerSpec.Env). + BaseEnv map[string]string `protobuf:"bytes,3,rep,name=base_env,json=baseEnv,proto3" json:"base_env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionRequest) Reset() { + *x = ProvisionRequest{} + mi := &file_compass_v1_guest_control_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionRequest) ProtoMessage() {} + +func (x *ProvisionRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionRequest.ProtoReflect.Descriptor instead. +func (*ProvisionRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{12} +} + +func (x *ProvisionRequest) GetNftScript() string { + if x != nil { + return x.NftScript + } + return "" +} + +func (x *ProvisionRequest) GetDefaultExecUid() uint32 { + if x != nil { + return x.DefaultExecUid + } + return 0 +} + +func (x *ProvisionRequest) GetBaseEnv() map[string]string { + if x != nil { + return x.BaseEnv + } + return nil +} + +type ProvisionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionResponse) Reset() { + *x = ProvisionResponse{} + mi := &file_compass_v1_guest_control_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionResponse) ProtoMessage() {} + +func (x *ProvisionResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_guest_control_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionResponse.ProtoReflect.Descriptor instead. +func (*ProvisionResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_guest_control_proto_rawDescGZIP(), []int{13} +} + var File_compass_v1_guest_control_proto protoreflect.FileDescriptor const file_compass_v1_guest_control_proto_rawDesc = "" + "\n" + "\x1ecompass/v1/guest_control.proto\x12\n" + "compass.v1\"\x0f\n" + - "\rHealthRequest\"\x8d\x01\n" + + "\rHealthRequest\"\xac\x01\n" + "\x0eHealthResponse\x12%\n" + "\x0eguestd_version\x18\x01 \x01(\tR\rguestdVersion\x12'\n" + "\x0fnet_provisioned\x18\x02 \x01(\bR\x0enetProvisioned\x12+\n" + - "\x11workspace_mounted\x18\x03 \x01(\bR\x10workspaceMounted2O\n" + + "\x11workspace_mounted\x18\x03 \x01(\bR\x10workspaceMounted\x12\x1d\n" + + "\n" + + "boot_nonce\x18\x04 \x01(\fR\tbootNonce\"\xab\x02\n" + + "\vExecRequest\x12\x18\n" + + "\acommand\x18\x01 \x03(\tR\acommand\x12\x15\n" + + "\x03uid\x18\x02 \x01(\rH\x00R\x03uid\x88\x01\x01\x12\x1d\n" + + "\aworkdir\x18\x03 \x01(\tH\x01R\aworkdir\x88\x01\x01\x122\n" + + "\x03env\x18\x04 \x03(\v2 .compass.v1.ExecRequest.EnvEntryR\x03env\x12\x19\n" + + "\x05stdin\x18\x05 \x01(\fH\x02R\x05stdin\x88\x01\x01\x12'\n" + + "\x0ftimeout_seconds\x18\x06 \x01(\rR\x0etimeoutSeconds\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" + + "\x04_uidB\n" + + "\n" + + "\b_workdirB\b\n" + + "\x06_stdin\"[\n" + + "\fExecResponse\x12\x16\n" + + "\x06stdout\x18\x01 \x01(\fR\x06stdout\x12\x16\n" + + "\x06stderr\x18\x02 \x01(\fR\x06stderr\x12\x1b\n" + + "\texit_code\x18\x03 \x01(\x05R\bexitCode\"\x9e\x01\n" + + "\x11ExecStreamRequest\x12-\n" + + "\x05start\x18\x01 \x01(\v2\x15.compass.v1.StartExecH\x00R\x05start\x12\x16\n" + + "\x05stdin\x18\x02 \x01(\fH\x00R\x05stdin\x129\n" + + "\vstdin_close\x18\x03 \x01(\v2\x16.compass.v1.StdinCloseH\x00R\n" + + "stdinCloseB\a\n" + + "\x05frame\"\xd9\x01\n" + + "\tStartExec\x12\x18\n" + + "\acommand\x18\x01 \x03(\tR\acommand\x12\x15\n" + + "\x03uid\x18\x02 \x01(\rH\x00R\x03uid\x88\x01\x01\x12\x1d\n" + + "\aworkdir\x18\x03 \x01(\tH\x01R\aworkdir\x88\x01\x01\x120\n" + + "\x03env\x18\x04 \x03(\v2\x1e.compass.v1.StartExec.EnvEntryR\x03env\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" + + "\x04_uidB\n" + + "\n" + + "\b_workdir\"\f\n" + + "\n" + + "StdinClose\"\xb2\x01\n" + + "\x12ExecStreamResponse\x123\n" + + "\astarted\x18\x01 \x01(\v2\x17.compass.v1.ExecStartedH\x00R\astarted\x12\x18\n" + + "\x06stdout\x18\x02 \x01(\fH\x00R\x06stdout\x12\x18\n" + + "\x06stderr\x18\x03 \x01(\fH\x00R\x06stderr\x12*\n" + + "\x04exit\x18\x04 \x01(\v2\x14.compass.v1.ExecExitH\x00R\x04exitB\a\n" + + "\x05frame\"&\n" + + "\vExecStarted\x12\x17\n" + + "\aexec_id\x18\x01 \x01(\tR\x06execId\"?\n" + + "\bExecExit\x12\x1b\n" + + "\texit_code\x18\x01 \x01(\x05R\bexitCode\x12\x16\n" + + "\x06signal\x18\x02 \x01(\x05R\x06signal\"@\n" + + "\rSignalRequest\x12\x17\n" + + "\aexec_id\x18\x01 \x01(\tR\x06execId\x12\x16\n" + + "\x06signal\x18\x02 \x01(\x05R\x06signal\"\x10\n" + + "\x0eSignalResponse\"\xdd\x01\n" + + "\x10ProvisionRequest\x12\x1d\n" + + "\n" + + "nft_script\x18\x01 \x01(\tR\tnftScript\x12(\n" + + "\x10default_exec_uid\x18\x02 \x01(\rR\x0edefaultExecUid\x12D\n" + + "\bbase_env\x18\x03 \x03(\v2).compass.v1.ProvisionRequest.BaseEnvEntryR\abaseEnv\x1a:\n" + + "\fBaseEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x13\n" + + "\x11ProvisionResponse2\xe6\x02\n" + "\fGuestControl\x12?\n" + - "\x06Health\x12\x19.compass.v1.HealthRequest\x1a\x1a.compass.v1.HealthResponseb\x06proto3" + "\x06Health\x12\x19.compass.v1.HealthRequest\x1a\x1a.compass.v1.HealthResponse\x129\n" + + "\x04Exec\x12\x17.compass.v1.ExecRequest\x1a\x18.compass.v1.ExecResponse\x12O\n" + + "\n" + + "ExecStream\x12\x1d.compass.v1.ExecStreamRequest\x1a\x1e.compass.v1.ExecStreamResponse(\x010\x01\x12?\n" + + "\x06Signal\x12\x19.compass.v1.SignalRequest\x1a\x1a.compass.v1.SignalResponse\x12H\n" + + "\tProvision\x12\x1c.compass.v1.ProvisionRequest\x1a\x1d.compass.v1.ProvisionResponseb\x06proto3" var ( file_compass_v1_guest_control_proto_rawDescOnce sync.Once @@ -172,19 +1021,49 @@ func file_compass_v1_guest_control_proto_rawDescGZIP() []byte { return file_compass_v1_guest_control_proto_rawDescData } -var file_compass_v1_guest_control_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_compass_v1_guest_control_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_compass_v1_guest_control_proto_goTypes = []any{ - (*HealthRequest)(nil), // 0: compass.v1.HealthRequest - (*HealthResponse)(nil), // 1: compass.v1.HealthResponse + (*HealthRequest)(nil), // 0: compass.v1.HealthRequest + (*HealthResponse)(nil), // 1: compass.v1.HealthResponse + (*ExecRequest)(nil), // 2: compass.v1.ExecRequest + (*ExecResponse)(nil), // 3: compass.v1.ExecResponse + (*ExecStreamRequest)(nil), // 4: compass.v1.ExecStreamRequest + (*StartExec)(nil), // 5: compass.v1.StartExec + (*StdinClose)(nil), // 6: compass.v1.StdinClose + (*ExecStreamResponse)(nil), // 7: compass.v1.ExecStreamResponse + (*ExecStarted)(nil), // 8: compass.v1.ExecStarted + (*ExecExit)(nil), // 9: compass.v1.ExecExit + (*SignalRequest)(nil), // 10: compass.v1.SignalRequest + (*SignalResponse)(nil), // 11: compass.v1.SignalResponse + (*ProvisionRequest)(nil), // 12: compass.v1.ProvisionRequest + (*ProvisionResponse)(nil), // 13: compass.v1.ProvisionResponse + nil, // 14: compass.v1.ExecRequest.EnvEntry + nil, // 15: compass.v1.StartExec.EnvEntry + nil, // 16: compass.v1.ProvisionRequest.BaseEnvEntry } var file_compass_v1_guest_control_proto_depIdxs = []int32{ - 0, // 0: compass.v1.GuestControl.Health:input_type -> compass.v1.HealthRequest - 1, // 1: compass.v1.GuestControl.Health:output_type -> compass.v1.HealthResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 14, // 0: compass.v1.ExecRequest.env:type_name -> compass.v1.ExecRequest.EnvEntry + 5, // 1: compass.v1.ExecStreamRequest.start:type_name -> compass.v1.StartExec + 6, // 2: compass.v1.ExecStreamRequest.stdin_close:type_name -> compass.v1.StdinClose + 15, // 3: compass.v1.StartExec.env:type_name -> compass.v1.StartExec.EnvEntry + 8, // 4: compass.v1.ExecStreamResponse.started:type_name -> compass.v1.ExecStarted + 9, // 5: compass.v1.ExecStreamResponse.exit:type_name -> compass.v1.ExecExit + 16, // 6: compass.v1.ProvisionRequest.base_env:type_name -> compass.v1.ProvisionRequest.BaseEnvEntry + 0, // 7: compass.v1.GuestControl.Health:input_type -> compass.v1.HealthRequest + 2, // 8: compass.v1.GuestControl.Exec:input_type -> compass.v1.ExecRequest + 4, // 9: compass.v1.GuestControl.ExecStream:input_type -> compass.v1.ExecStreamRequest + 10, // 10: compass.v1.GuestControl.Signal:input_type -> compass.v1.SignalRequest + 12, // 11: compass.v1.GuestControl.Provision:input_type -> compass.v1.ProvisionRequest + 1, // 12: compass.v1.GuestControl.Health:output_type -> compass.v1.HealthResponse + 3, // 13: compass.v1.GuestControl.Exec:output_type -> compass.v1.ExecResponse + 7, // 14: compass.v1.GuestControl.ExecStream:output_type -> compass.v1.ExecStreamResponse + 11, // 15: compass.v1.GuestControl.Signal:output_type -> compass.v1.SignalResponse + 13, // 16: compass.v1.GuestControl.Provision:output_type -> compass.v1.ProvisionResponse + 12, // [12:17] is the sub-list for method output_type + 7, // [7:12] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_compass_v1_guest_control_proto_init() } @@ -192,13 +1071,26 @@ func file_compass_v1_guest_control_proto_init() { if File_compass_v1_guest_control_proto != nil { return } + file_compass_v1_guest_control_proto_msgTypes[2].OneofWrappers = []any{} + file_compass_v1_guest_control_proto_msgTypes[4].OneofWrappers = []any{ + (*ExecStreamRequest_Start)(nil), + (*ExecStreamRequest_Stdin)(nil), + (*ExecStreamRequest_StdinClose)(nil), + } + file_compass_v1_guest_control_proto_msgTypes[5].OneofWrappers = []any{} + file_compass_v1_guest_control_proto_msgTypes[7].OneofWrappers = []any{ + (*ExecStreamResponse_Started)(nil), + (*ExecStreamResponse_Stdout)(nil), + (*ExecStreamResponse_Stderr)(nil), + (*ExecStreamResponse_Exit)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_compass_v1_guest_control_proto_rawDesc), len(file_compass_v1_guest_control_proto_rawDesc)), NumEnums: 0, - NumMessages: 2, + NumMessages: 17, NumExtensions: 0, NumServices: 1, }, diff --git a/go/internal/guestd/health.go b/go/internal/guestd/health.go index ff1fc8618..6b57d433e 100644 --- a/go/internal/guestd/health.go +++ b/go/internal/guestd/health.go @@ -11,12 +11,18 @@ import ( "github.com/RigelBuild/compass/go/internal/gen/compass/v1/compassv1internalconnect" ) -// healthService implements the generated GuestControlHandler's single RPC, -// Health (§(e)). It is constructed only after net + mount both succeed, so its -// fields are the completed boot state — a successful handshake IS the proof of -// bringup (§(d) fail-closed invariant). The values are immutable once set at +// healthService implements the generated GuestControlHandler's Health RPC +// (§(e)). It is constructed only after net + mount both succeed, so its fields +// are the completed boot state — a successful handshake IS the proof of bringup +// (§(d) fail-closed invariant). The values are immutable once set at // construction, so Health is safe to serve concurrently with no locking. +// +// U1 grew GuestControl with the exec surface (Exec/ExecStream/Signal/Provision); +// until U2 fills them in the real supervisor, healthService embeds +// UnimplementedGuestControlHandler so those four RPCs answer CodeUnimplemented +// and the V2a Health path is byte-unchanged. U2 replaces this type outright. type healthService struct { + compassv1internalconnect.UnimplementedGuestControlHandler version string netProvisioned bool workspaceMounted bool diff --git a/go/internal/runtime/microvm/dial_test.go b/go/internal/runtime/microvm/dial_test.go index db4962894..19387fc99 100644 --- a/go/internal/runtime/microvm/dial_test.go +++ b/go/internal/runtime/microvm/dial_test.go @@ -237,8 +237,12 @@ func TestDialGuest_HandshakeDeadline(t *testing.T) { } // healthHandler is a canned GuestControl handler: every Health returns the same -// response, so the round-trip test asserts the fields survive the wire. +// response, so the round-trip test asserts the fields survive the wire. It +// embeds UnimplementedGuestControlHandler so it satisfies the exec surface U1 +// grew onto GuestControl (Exec/ExecStream/Signal/Provision) without this V2a +// test exercising them. type healthHandler struct { + compassv1internalconnect.UnimplementedGuestControlHandler resp *compassv1.HealthResponse } diff --git a/proto/compass/v1/guest_control.proto b/proto/compass/v1/guest_control.proto index bbba72d42..85c6c6934 100644 --- a/proto/compass/v1/guest_control.proto +++ b/proto/compass/v1/guest_control.proto @@ -37,15 +37,47 @@ package compass.v1; // Suffix-less service name follows the AgentGateway precedent (a Runner/host-side // door, not a public *Service) — carved out per-file in buf.yaml. // -// The transport is unauthenticated: no credential travels the vsock hop. For -// V2a Health this is correct — the host owns both ends of the hybrid vsock and -// the AF_UNIX socket path is the session's structural identity, so "it answered -// on the vsock we opened" is sufficient. When V2b adds the higher-value RPCs -// (Exec/Signal/Provision), that assumption must be revisited explicitly: an Exec -// surface may warrant the host authenticating guestd's identity beyond reaching -// it on the expected socket. +// The transport carries no credential over the vsock hop, by design (OQ-A +// resolved, record §(e)): the boundary is structural, not a secret. The host +// owns both ends of the hybrid vsock and the per-session AF_UNIX endpoint lives +// in a runtime dir only the Runner's uid can reach, so "it answered on the +// socket we opened" is the host's proof of the guest's identity. guestd +// independently refuses any vsock peer that is not the host (CID 2), closing the +// in-guest loopback (CID 1). As a cheap binding against a swapped guest, the +// host mints a boot nonce on the kernel cmdline (compass.boot_nonce=) that +// guestd echoes in HealthResponse.boot_nonce; Start verifies it before opening +// the exec gate. This is an identity binding (right guest on the right socket), +// not a secret channel — any nonce guestd can present, a guest-root attacker can +// read from /proc/cmdline, so a bearer token would buy no boundary the VM + +// socket ownership do not already provide. service GuestControl { rpc Health(HealthRequest) returns (HealthResponse); + + // Exec runs one command to completion and returns its captured output. A + // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is + // non-zero — never a Connect error. A Connect error means the exec could not + // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an + // unprovisioned gate. + rpc Exec(ExecRequest) returns (ExecResponse); + + // ExecStream runs a long-lived command over one bidi stream: the request + // stream carries the start frame, then stdin bytes and an optional + // half-close; the response stream carries the exec_id, then interleaved + // stdout/stderr, then exactly one terminal exit frame. Kill rides Signal + // (below), not this stream, so a teardown never contends with an in-flight + // stdin write. + rpc ExecStream(stream ExecStreamRequest) returns (stream ExecStreamResponse); + + // Signal delivers a signal to a running exec by exec_id; an empty exec_id + // targets the guest itself (graceful Stop, record §(d)). Signalling an + // already-exited exec is a no-op success. + rpc Signal(SignalRequest) returns (SignalResponse); + + // Provision transitions the supervisor ready -> provisioned: it records the + // session's default exec uid and base env and, when nft_script is non-empty, + // arms egress (V3; a non-empty script is unimplemented in V2b). Exec and + // ExecStream are refused until Provision succeeds. + rpc Provision(ProvisionRequest) returns (ProvisionResponse); } message HealthRequest {} @@ -54,4 +86,103 @@ message HealthResponse { string guestd_version = 1; bool net_provisioned = 2; bool workspace_mounted = 3; + // boot_nonce echoes the host-minted compass.boot_nonce cmdline value so Start + // can bind this guest to the VM it launched before opening the exec gate + // (record §(e), OQ-A). Empty until V2b guestd parses the cmdline nonce. + bytes boot_nonce = 4; } + +// ExecRequest is a one-shot exec. It mirrors runtime.ExecSpec field for field. +message ExecRequest { + // command is argv; command[0] is the program. + repeated string command = 1; + // uid is the exec's user. Absent = the session default uid set by Provision + // (the baked agent uid). uid 0 is REFUSED with a failed-precondition error + // before any spawn — the guest supervisor never runs an exec as root. + optional uint32 uid = 2; + // workdir is the working directory; absent = the child's default. + optional string workdir = 3; + // env is merged over the session base env; exec-specific keys win. + map env = 4; + // stdin is fed to the child's stdin — the body over the wire, NEVER the argv, + // so a script and any secret it embeds never appear in the guest process + // list. + optional bytes stdin = 5; + // timeout_seconds bounds the command; the guest enforces it too. + uint32 timeout_seconds = 6; +} + +message ExecResponse { + bytes stdout = 1; + bytes stderr = 2; + // exit_code is the command's exit status. Non-zero is a successful call with + // a failed command, never a Connect error (see the Exec doc-comment). + int32 exit_code = 3; +} + +// ExecStreamRequest frames flow host -> guest: exactly one StartExec first, +// then any number of stdin frames, then an optional StdinClose half-close. +message ExecStreamRequest { + oneof frame { + StartExec start = 1; + bytes stdin = 2; + StdinClose stdin_close = 3; + } +} + +message StartExec { + repeated string command = 1; + // uid as in ExecRequest: absent = session default; 0 REFUSED. + optional uint32 uid = 2; + optional string workdir = 3; + map env = 4; +} + +// StdinClose half-closes the child's stdin pipe without ending the stream. +message StdinClose {} + +// ExecStreamResponse frames flow guest -> host: exactly one ExecStarted first, +// then interleaved stdout/stderr, then exactly one ExecExit. +message ExecStreamResponse { + oneof frame { + ExecStarted started = 1; + bytes stdout = 2; + bytes stderr = 3; + ExecExit exit = 4; + } +} + +// ExecStarted is the first response frame; exec_id is the handle Signal targets. +message ExecStarted { + string exec_id = 1; +} + +// ExecExit is the terminal response frame, emitted from guestd's own reap. +message ExecExit { + // exit_code is meaningful when signal == 0. + int32 exit_code = 1; + // signal is non-zero when the child died by signal (e.g. SIGKILL on Kill). + int32 signal = 2; +} + +message SignalRequest { + // exec_id targets a running exec; "" targets the guest itself (Stop). + string exec_id = 1; + // signal is the signal number: SIGKILL for a Kill, SIGTERM for Stop. + int32 signal = 2; +} + +message SignalResponse {} + +message ProvisionRequest { + // nft_script is the egress ruleset (V3, EgressPolicy.NftScript()); empty in + // V2b, where a non-empty value is unimplemented. + string nft_script = 1; + // default_exec_uid is the session's agent uid (ContainerSpec.UID), the + // default for an exec with no uid. Validated non-zero. + uint32 default_exec_uid = 2; + // base_env is the base environment every exec inherits (ContainerSpec.Env). + map base_env = 3; +} + +message ProvisionResponse {} From 1d0d884a29116b4dc06840fcc4029d1b824f1a35 Mon Sep 17 00:00:00 2001 From: mintaka Date: Thu, 27 Aug 2026 22:45:56 -0400 Subject: [PATCH 2/2] fix(guest_control): tighten the Exec error-contract doc-comment (RIG-2493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address ReviewU1's two low findings on the `Exec` RPC doc-comment (proto + regenerated connect interface): - The `(0/caps)` parenthetical implied capabilities are refusable over this proto; they are not — caps are ignored-and-asserted in the guest, never surfaced as a Connect error. Narrowed to `(0)`, since a refused uid is the only wire-refusable case. - "could not be attempted" excluded the attempted-then-timed-out case. Reworded to "could not be attempted or could not complete" so a spawn failure, a refused uid, a timeout, and an unprovisioned gate all read as covered. Doc-only; the wire surface is byte-identical. `moon run compass-proto:ci` green (drift confirms generated matches source). Ledger-impact: none Spec-impact: none. Refs RIG-2493 Co-authored-by: Matt Wilkinson --- .../v1/compassv1internalconnect/guest_control.connect.go | 8 ++++---- proto/compass/v1/guest_control.proto | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go b/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go index ec1768adf..25ebd31a3 100644 --- a/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go +++ b/go/internal/gen/compass/v1/compassv1internalconnect/guest_control.connect.go @@ -80,8 +80,8 @@ type GuestControlClient interface { // Exec runs one command to completion and returns its captured output. A // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is // non-zero — never a Connect error. A Connect error means the exec could not - // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an - // unprovisioned gate. + // be attempted or could not complete: a spawn failure, a refused uid (0), a + // timeout, or an unprovisioned gate. Exec(context.Context, *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) // ExecStream runs a long-lived command over one bidi stream: the request // stream carries the start frame, then stdin bytes and an optional @@ -185,8 +185,8 @@ type GuestControlHandler interface { // Exec runs one command to completion and returns its captured output. A // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is // non-zero — never a Connect error. A Connect error means the exec could not - // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an - // unprovisioned gate. + // be attempted or could not complete: a spawn failure, a refused uid (0), a + // timeout, or an unprovisioned gate. Exec(context.Context, *connect.Request[v1.ExecRequest]) (*connect.Response[v1.ExecResponse], error) // ExecStream runs a long-lived command over one bidi stream: the request // stream carries the start frame, then stdin bytes and an optional diff --git a/proto/compass/v1/guest_control.proto b/proto/compass/v1/guest_control.proto index 85c6c6934..94d653bdb 100644 --- a/proto/compass/v1/guest_control.proto +++ b/proto/compass/v1/guest_control.proto @@ -56,8 +56,8 @@ service GuestControl { // Exec runs one command to completion and returns its captured output. A // non-zero exit is a SUCCESSFUL call whose ExecResponse.exit_code is // non-zero — never a Connect error. A Connect error means the exec could not - // be attempted: a spawn failure, a refused uid (0/caps), a timeout, or an - // unprovisioned gate. + // be attempted or could not complete: a spawn failure, a refused uid (0), a + // timeout, or an unprovisioned gate. rpc Exec(ExecRequest) returns (ExecResponse); // ExecStream runs a long-lived command over one bidi stream: the request