// Package supervisor defines the narrow process-control seam shared by the // client runtime and platform implementations. It intentionally contains no // operating-system handles or process APIs. package supervisor import ( "context" "errors" "time" rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" "github.com/rvbox/rvbox/internal/domain" ) var ( ErrInvalidStartSpec = errors.New("invalid supervisor start specification") ErrUnsupported = errors.New("supervisor operation is unsupported") ) // StartError carries durable context-selection evidence when a command is // rejected before a process exists. The runtime records the evidence with // the rejection event without treating the error as a second public protocol // type. Unwrap keeps ordinary retry/error classification working. type StartError struct { Cause error WindowsIdentity *rvboxv1.WindowsExecutionIdentity } func (err *StartError) Error() string { if err == nil || err.Cause == nil { return "supervisor start failed" } return err.Cause.Error() } func (err *StartError) Unwrap() error { if err == nil { return nil } return err.Cause } type SignalKind uint8 const ( SignalTerm SignalKind = iota + 1 SignalKill ) // StartSpec is already validated at the protocol boundary. The supervisor // still checks the identity/revision/source invariants because it is a second // durability boundary and may be called after a restart. type StartSpec struct { IssueUUID domain.UUID CommandRevision uint64 Execution *rvboxv1.ExecutionSpec // ScriptBody is the verified, durable body for Execution.script. It is // supplied by the client spool only after the declared digest/length have // been checked; command_text requests leave it empty. ScriptBody []byte WorkingDirectory string Environment map[string]string ExecutionProfiles []string } func (spec StartSpec) Validate() error { if _, err := domain.ParseUUIDv7(spec.IssueUUID.String()); err != nil || spec.CommandRevision == 0 || spec.Execution == nil || spec.Execution.Source == nil { return ErrInvalidStartSpec } if spec.WorkingDirectory == "" { return ErrInvalidStartSpec } return nil } // EffectiveIdentity is immutable process evidence captured before launch. // Empty user/session fields mean a Session 0 service context. type EffectiveIdentity struct { Context string SessionID uint32 SessionUserSID string UserSID string LogonSID string Elevated bool Integrity string AttemptedContexts []string SelectionDetail string } // WindowsIdentity converts the platform-neutral evidence to the public // immutable status/event shape. Unknown contexts are deliberately omitted so // the portable test supervisor never pretends to be a Windows launch. func (identity EffectiveIdentity) WindowsIdentity() *rvboxv1.WindowsExecutionIdentity { result := &rvboxv1.WindowsExecutionIdentity{SessionUserSid: identity.SessionUserSID, EffectiveUserSid: identity.UserSID, SelectionDetail: identity.SelectionDetail} for _, contextName := range identity.AttemptedContexts { if context, ok := windowsExecutionContext(contextName); ok { result.AttemptedContexts = append(result.AttemptedContexts, context) } } if context, ok := windowsExecutionContext(identity.Context); ok { result.EffectiveContext = &context if context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM { session := identity.SessionID result.SessionId = &session } } if result.EffectiveContext == nil && len(result.AttemptedContexts) == 0 && result.SelectionDetail == "" && result.SessionUserSid == "" && result.EffectiveUserSid == "" { return nil } return result } func windowsExecutionContext(value string) (rvboxv1.WindowsExecutionContext, bool) { switch value { case "LOCAL_SERVICE": return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SERVICE, true case "LOCAL_SYSTEM": return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SYSTEM, true case "ACTIVE_USER": return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER, true case "ACTIVE_USER_ELEVATED": return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED, true case "ACTIVE_SYSTEM": return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM, true default: return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_UNSPECIFIED, false } } type Process interface { IssueUUID() domain.UUID Identity() EffectiveIdentity // ReadOutput returns the next bounded stdout/stderr chunk. It continues // until both child pipes reach EOF, so Wait never reports a terminal // result before the captured output has drained. ReadOutput(context.Context) (OutputChunk, error) Wait(context.Context) (ExitStatus, error) WriteStdin(context.Context, []byte, bool) error CloseStdin(context.Context) error } // OutputChunk is intentionally raw. Compression, quota admission, local // ordering, and wire sequencing belong to the client spool rather than the // operating-system supervisor. type OutputChunk struct { Stream rvboxv1.StreamKind Data []byte } type ExitStatus struct { Code int32 Signaled bool Signal SignalKind StartedAt time.Time FinishedAt time.Time OutputDrained bool Output bool } type SignalOutcome struct { Delivered bool Escalated bool Detail string ObservedAt time.Time } type ResourceSnapshot struct { CPUTime time.Duration ResidentBytes uint64 IOReadBytes uint64 IOWriteBytes uint64 ProcessCount uint64 ObservedAt time.Time Complete bool Detail string } type Supervisor interface { Start(context.Context, StartSpec) (Process, error) Signal(context.Context, Process, SignalKind) (SignalOutcome, error) Snapshot(context.Context, Process) (ResourceSnapshot, error) StopAll(context.Context) error }