// Package launch contains the core-only, fail-closed application startup use // case. Platform process, compatibility and process-creation capabilities are // supplied by the caller. package launch import ( "errors" "fmt" "os" "regexp" "softbox.local/core/internal/safepath" "softbox.local/core/storage" ) var ( ErrLaunchConfig = errors.New("invalid launch service configuration") ErrLaunchRequest = errors.New("invalid launch request") ErrAppNotInstalled = errors.New("app is not installed") ErrLaunchMetadata = errors.New("installed launch metadata is invalid") ErrLaunchTargetUnsafe = errors.New("installed launch target is unsafe") ErrEntrypointMissing = errors.New("installed entrypoint is missing") ErrCompatibilityCheck = errors.New("system compatibility check failed") ErrAppIncompatible = errors.New("installed app is incompatible with this system") ErrAuthorizationCheck = errors.New("launch authorization check failed") ErrLaunchUnauthorized = errors.New("launch is not authorized") ErrTargetStateCheck = errors.New("launch target state check failed") ErrAppRunning = errors.New("installed app is already running") ErrProcessStart = errors.New("start installed app") launchAppIDPattern = regexp.MustCompile(`^[a-z0-9-]+$`) ) // FailureCode is the stable, non-localized result of a launch attempt. type FailureCode string const ( FailureCodeNotInstalled FailureCode = "not_installed" FailureCodeLaunchMetadataInvalid FailureCode = "launch_metadata_invalid" FailureCodeLaunchTargetUnsafe FailureCode = "launch_target_unsafe" FailureCodeEntrypointMissing FailureCode = "entrypoint_missing" FailureCodeCompatibilityUnavailable FailureCode = "compatibility_unavailable" FailureCodeAppIncompatible FailureCode = "app_incompatible" FailureCodeAuthorizationFailed FailureCode = "authorization_unavailable" FailureCodeLaunchUnauthorized FailureCode = "not_authorized" FailureCodeAppRunning FailureCode = "app_running" FailureCodeTargetStateUnavailable FailureCode = "target_state_unavailable" FailureCodeLaunchFailed FailureCode = "launch_failed" ) // Error preserves a stable launch code and the diagnostic cause. type Error struct { Code FailureCode Err error } func (err *Error) Error() string { return fmt.Sprintf("launch (%s): %v", err.Code, err.Err) } func (err *Error) Unwrap() error { return err.Err } // Request names the installed app to start. The caller cannot supply a path, // arguments or working directory. type Request struct { AppID string } // Command contains only paths resolved from verified installed metadata. type Command struct { Entrypoint string WorkingDirectory string RequiresAdmin bool } // Result is returned only after the platform accepted the process start. type Result struct { AppID string PID int } // InstalledAppResolver returns an installed record paired with a validated // real current directory. type InstalledAppResolver interface { ResolveCurrent(appID string) (storage.InstalledApp, string, error) } // CompatibilityChecker reports whether a recorded min_os may run here. type CompatibilityChecker interface { IsCompatible(minOS string) (bool, error) } // AuthorizationChecker decides whether the user may launch one app. It is a // required boundary; license policy is implemented by the later licensing task. type AuthorizationChecker interface { IsAuthorized(appID string) (bool, error) } // TargetStateChecker reports whether this precise entrypoint is running. type TargetStateChecker interface { IsRunning(appID string, entrypointPath string) (bool, error) } // ProcessLauncher starts one verified command without accepting shell input. type ProcessLauncher interface { Start(command Command) (int, error) } // ServiceConfig makes every external launch dependency explicit. type ServiceConfig struct { Records InstalledAppResolver Compatibility CompatibilityChecker Authorization AuthorizationChecker TargetState TargetStateChecker Launcher ProcessLauncher } // Service executes the safe local launch flow. type Service struct { records InstalledAppResolver compatibility CompatibilityChecker authorization AuthorizationChecker targetState TargetStateChecker launcher ProcessLauncher } func NewService(config ServiceConfig) (*Service, error) { if config.Records == nil { return nil, fmt.Errorf("%w: installed app resolver is required", ErrLaunchConfig) } if config.Compatibility == nil { return nil, fmt.Errorf("%w: compatibility checker is required", ErrLaunchConfig) } if config.Authorization == nil { return nil, fmt.Errorf("%w: authorization checker is required", ErrLaunchConfig) } if config.TargetState == nil { return nil, fmt.Errorf("%w: target state checker is required", ErrLaunchConfig) } if config.Launcher == nil { return nil, fmt.Errorf("%w: process launcher is required", ErrLaunchConfig) } return &Service{ records: config.Records, compatibility: config.Compatibility, authorization: config.Authorization, targetState: config.TargetState, launcher: config.Launcher, }, nil } // Start validates local metadata and filesystem identity before invoking the // platform process launcher. func (service *Service) Start(request Request) (Result, error) { if !launchAppIDPattern.MatchString(request.AppID) { return Result{}, launchError(ErrLaunchRequest) } record, current, err := service.records.ResolveCurrent(request.AppID) if err != nil { if errors.Is(err, os.ErrNotExist) { return Result{}, launchError(ErrAppNotInstalled) } if errors.Is(err, storage.ErrStorageLayoutUnsafe) { return Result{}, launchError(fmt.Errorf("%w: %w", ErrLaunchTargetUnsafe, err)) } return Result{}, launchError(fmt.Errorf("resolve installed app: %w", err)) } command, err := launchCommand(record, current) if err != nil { return Result{}, launchError(err) } compatible, err := service.compatibility.IsCompatible(record.MinOS) if err != nil { return Result{}, launchError(fmt.Errorf("%w: %w", ErrCompatibilityCheck, err)) } if !compatible { return Result{}, launchError(ErrAppIncompatible) } authorized, err := service.authorization.IsAuthorized(record.ID) if err != nil { return Result{}, launchError(fmt.Errorf("%w: %w", ErrAuthorizationCheck, err)) } if !authorized { return Result{}, launchError(ErrLaunchUnauthorized) } running, err := service.targetState.IsRunning(record.ID, command.Entrypoint) if err != nil { return Result{}, launchError(fmt.Errorf("%w: %w", ErrTargetStateCheck, err)) } if running { return Result{}, launchError(ErrAppRunning) } pid, err := service.launcher.Start(command) if err != nil { return Result{}, launchError(fmt.Errorf("%w: %w", ErrProcessStart, err)) } if pid <= 0 { return Result{}, launchError(fmt.Errorf("%w: invalid process ID", ErrProcessStart)) } return Result{AppID: record.ID, PID: pid}, nil } func launchCommand(record storage.InstalledApp, current string) (Command, error) { if record.Entrypoint == "" || record.WorkingDirectory == "" || record.MinOS == "" { return Command{}, ErrLaunchMetadata } if err := safepath.ValidateRelative(record.Entrypoint); err != nil { return Command{}, fmt.Errorf("%w: entrypoint: %v", ErrLaunchMetadata, err) } if record.WorkingDirectory != "." { if err := safepath.ValidateRelative(record.WorkingDirectory); err != nil { return Command{}, fmt.Errorf("%w: working directory: %v", ErrLaunchMetadata, err) } } if !validMinOS(record.MinOS) { return Command{}, ErrLaunchMetadata } if !containsEntrypoint(record.Files, record.Entrypoint) { return Command{}, fmt.Errorf("%w: entrypoint is not in installed files", ErrLaunchMetadata) } if err := requireRealDirectory(current); err != nil { return Command{}, err } entrypoint, err := safepath.JoinUnder(current, record.Entrypoint) if err != nil { return Command{}, fmt.Errorf("%w: entrypoint: %v", ErrLaunchTargetUnsafe, err) } workingDirectory := current if record.WorkingDirectory != "." { workingDirectory, err = safepath.JoinUnder(current, record.WorkingDirectory) if err != nil { return Command{}, fmt.Errorf("%w: working directory: %v", ErrLaunchTargetUnsafe, err) } } if err := requireRealDirectory(workingDirectory); err != nil { return Command{}, err } if err := requireRegularFile(entrypoint); err != nil { return Command{}, err } return Command{ Entrypoint: entrypoint, WorkingDirectory: workingDirectory, RequiresAdmin: record.RequiresAdmin, }, nil } func containsEntrypoint(files []storage.InstalledFile, entrypoint string) bool { for _, file := range files { if file.Path == entrypoint { return true } } return false } func validMinOS(minOS string) bool { return minOS == "windows-7-sp1" || minOS == "windows-10" || minOS == "windows-11" } func requireRealDirectory(path string) error { info, err := os.Lstat(path) if os.IsNotExist(err) { return fmt.Errorf("%w: %s", ErrLaunchTargetUnsafe, path) } if err != nil { return fmt.Errorf("%w: inspect %s: %w", ErrLaunchTargetUnsafe, path, err) } if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return fmt.Errorf("%w: %s is not a real directory", ErrLaunchTargetUnsafe, path) } return nil } func requireRegularFile(path string) error { info, err := os.Lstat(path) if os.IsNotExist(err) { return fmt.Errorf("%w: %s", ErrEntrypointMissing, path) } if err != nil { return fmt.Errorf("%w: inspect %s: %w", ErrLaunchTargetUnsafe, path, err) } if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { return fmt.Errorf("%w: %s is not a regular file", ErrLaunchTargetUnsafe, path) } return nil } func launchError(err error) error { return &Error{Code: failureCodeFor(err), Err: err} } func failureCodeFor(err error) FailureCode { switch { case errors.Is(err, ErrAppNotInstalled): return FailureCodeNotInstalled case errors.Is(err, ErrLaunchMetadata): return FailureCodeLaunchMetadataInvalid case errors.Is(err, ErrLaunchTargetUnsafe): return FailureCodeLaunchTargetUnsafe case errors.Is(err, ErrEntrypointMissing): return FailureCodeEntrypointMissing case errors.Is(err, ErrCompatibilityCheck): return FailureCodeCompatibilityUnavailable case errors.Is(err, ErrAppIncompatible): return FailureCodeAppIncompatible case errors.Is(err, ErrAuthorizationCheck): return FailureCodeAuthorizationFailed case errors.Is(err, ErrLaunchUnauthorized): return FailureCodeLaunchUnauthorized case errors.Is(err, ErrAppRunning): return FailureCodeAppRunning case errors.Is(err, ErrTargetStateCheck): return FailureCodeTargetStateUnavailable default: return FailureCodeLaunchFailed } }