package runtime import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "log" "net/url" "os" "os/exec" "path/filepath" "regexp" "runtime" "sort" "strconv" "strings" "sync" "time" "browser.local/run/protocol" ) const ( lifecycleResultStateSucceeded = "succeeded" lifecycleResultStateFailed = "failed" lifecycleResultStateCancelled = "cancelled" defaultLifecycleTimeout = 30 * time.Second maxLifecycleTimeout = 2 * time.Hour maxLifecycleOutputBytes = 4096 ) var ( commandNamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) envNamePattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,63}$`) disallowedExecutables = map[string]struct{}{ "bash": {}, "cmd": {}, "fish": {}, "powershell": {}, "pwsh": {}, "sh": {}, "zsh": {}, } ) type LifecycleExecutor struct { workspaceRoot string managedProcessStateRoot string managedProcessOutputRoot string supervisor ProcessSupervisor managed ManagedProcessSupervisor fileExecutor *FileExecutor logSink ProcessLogSink artifactHook LifecycleArtifactHook dependencyRunner ProcessSupervisor dependencyDownloader DependencyDownloader selfUpdateActivator SelfUpdateActivator logCheckpointStore LogCheckpointStore runtimeTargetOS string runtimeTargetArch string runtimeFileWriter runtimeFileWriter localStartupDiagnostics bool protectedRequests *ProtectedRequestRegistry sqliteSchemaProbe *SQLiteSchemaProbeExecutor sqliteQuery *SQLiteQueryExecutor metricCollector MetricCollector } type runtimeFileWriter func(string, []byte, os.FileMode) error type LifecycleExecutionResult struct { State string Progress protocol.RunJobProgressReport ResultRef string Message string ErrorCode string Retryable bool ExecutionResult protocol.RunJobExecutionResult ActivationManifest string } type LifecycleExecutorOption func(*LifecycleExecutor) func NewLifecycleExecutor(options ...LifecycleExecutorOption) LifecycleExecutor { executor := LifecycleExecutor{ workspaceRoot: filepath.Join(".", ".run-workspace"), supervisor: OSProcessSupervisor{}, logSink: NoopProcessLogSink{}, artifactHook: StaticLifecycleArtifactHook{}, dependencyRunner: OSProcessSupervisor{}, dependencyDownloader: HTTPDependencyDownloader{}, selfUpdateActivator: ProcessSelfUpdateActivator{}, logCheckpointStore: NewMemoryLogCheckpointStore(), runtimeTargetOS: runtime.GOOS, runtimeTargetArch: runtime.GOARCH, runtimeFileWriter: writeRuntimeAtomicFile, protectedRequests: NewProtectedRequestRegistry(), } for _, option := range options { option(&executor) } if executor.managedProcessStateRoot == "" { executor.managedProcessStateRoot = executor.workspaceRoot } if executor.managedProcessOutputRoot == "" { executor.managedProcessOutputRoot = executor.workspaceRoot } if executor.managed == nil { if managed, err := NewOSManagedProcessSupervisorWithOutputRoot(executor.managedProcessStateRoot, executor.managedProcessOutputRoot); err == nil { executor.managed = managed } } if files, err := NewFileExecutor(executor.workspaceRoot); err == nil { executor.fileExecutor = files } executor.sqliteSchemaProbe = NewSQLiteSchemaProbeExecutor(executor.workspaceRoot) executor.sqliteQuery = NewSQLiteQueryExecutor(executor.workspaceRoot) return executor } func (executor LifecycleExecutor) ExecuteLogBackfill(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult { if assignment.ExecutionInput.LogSource == nil { return ExecuteLogBackfillJob(ctx, assignment) } if err := protocol.ValidateRunJobAssignment(assignment); err != nil { return lifecycleFailure("unsafe_log_backfill_job", err.Error()) } source := RuntimeLogSource{ Key: assignment.ExecutionInput.LogSource.Key, Kind: assignment.ExecutionInput.LogSource.Kind, TargetKey: assignment.ExecutionInput.LogSource.TargetKey, StreamKey: assignment.ExecutionInput.LogSource.StreamKey, CursorKind: assignment.ExecutionInput.LogSource.CursorKind, RetentionDays: assignment.ExecutionInput.LogSource.RetentionDays, } return TailDeclaredFileLogSource(ctx, executor.workspaceRoot, assignment, source, executor.logSink, executor.logCheckpointStore) } func (executor LifecycleExecutor) writeRuntimeFile(path string, body []byte, mode os.FileMode) error { writer := executor.runtimeFileWriter if writer == nil { writer = writeRuntimeAtomicFile } return writer(path, body, mode) } func WithLifecycleWorkspaceRoot(root string) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if strings.TrimSpace(root) != "" { executor.workspaceRoot = root } } } func WithManagedProcessStateRoot(root string) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if strings.TrimSpace(root) != "" { executor.managedProcessStateRoot = root } } } func WithManagedProcessOutputRoot(root string) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if strings.TrimSpace(root) != "" { executor.managedProcessOutputRoot = root } } } func WithLocalStartupDiagnostics(enabled bool) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { executor.localStartupDiagnostics = enabled } } // WithProtectedRequestRegistry supplies Run-owned handlers for logical // transports. It does not expose handler configuration through any protocol. func WithProtectedRequestRegistry(registry *ProtectedRequestRegistry) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if registry != nil { executor.protectedRequests = registry } } } func WithProcessSupervisor(supervisor ProcessSupervisor) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if supervisor != nil { executor.supervisor = supervisor } } } func WithManagedProcessSupervisor(supervisor ManagedProcessSupervisor) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if supervisor != nil { executor.managed = supervisor } } } func WithProcessLogSink(sink ProcessLogSink) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if sink != nil { executor.logSink = sink } } } func WithLifecycleArtifactHook(hook LifecycleArtifactHook) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if hook != nil { executor.artifactHook = hook } } } func WithDependencyCommandRunner(runner ProcessSupervisor) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if runner != nil { executor.dependencyRunner = runner } } } func WithDependencyDownloader(downloader DependencyDownloader) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if downloader != nil { executor.dependencyDownloader = downloader } } } func WithSelfUpdateActivator(activator SelfUpdateActivator) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if activator != nil { executor.selfUpdateActivator = activator } } } // WithDLLExtensionRuntimeTarget is primarily useful for exercising the // Windows-only extension path in cross-platform tests. Production workers use // the current Go runtime target. func WithDLLExtensionRuntimeTarget(targetOS string, targetArch string) LifecycleExecutorOption { return func(executor *LifecycleExecutor) { if strings.TrimSpace(targetOS) != "" { executor.runtimeTargetOS = strings.ToLower(strings.TrimSpace(targetOS)) } if strings.TrimSpace(targetArch) != "" { executor.runtimeTargetArch = strings.ToLower(strings.TrimSpace(targetArch)) } } } func SupportedLifecycleCapabilities() []string { return []string{ protocol.RunCapabilityProcessInstall, protocol.RunCapabilityProcessStart, protocol.RunCapabilityProcessStop, protocol.RunCapabilityProcessStatus, } } func SupportedFileCapabilities() []string { return []string{protocol.RunCapabilityConfigWrite, protocol.RunCapabilityFilesList, protocol.RunCapabilityFilesRead, protocol.RunCapabilityFilesWrite} } func SupportedRunCapabilities() []string { return SupportedRunCapabilitiesForComponent("") } // SupportedRunCapabilitiesForComponent keeps a generic worker capable of // building distributions while ensuring a generated server Run cannot claim // a shared build-worker endpoint after it is deployed. func SupportedRunCapabilitiesForComponent(componentKind string) []string { capabilities := append([]string(nil), SupportedLifecycleCapabilities()...) capabilities = append(capabilities, SupportedFileCapabilities()...) capabilities = append(capabilities, protocol.RunCapabilityLogsRead) capabilities = append(capabilities, protocol.RunCapabilityDeploymentPlan) for _, capability := range SupportedDistributionCapabilities() { if componentKind == "run" && capability == protocol.RunCapabilityDistributionBuild { continue } capabilities = append(capabilities, capability) } capabilities = append(capabilities, SupportedRemoteCapabilities()...) return capabilities } func SupportedRemoteCapabilities() []string { return []string{ protocol.RunCapabilityRemoteFTPRead, protocol.RunCapabilityRemoteFTPWrite, protocol.RunCapabilityRemoteRsyncRead, protocol.RunCapabilityRemoteRsyncWrite, protocol.RunCapabilityRemoteRunFilesRead, protocol.RunCapabilityRemoteRunFilesWrite, protocol.RunCapabilityRemoteRunProcessStart, protocol.RunCapabilityRemoteRunProcessStop, protocol.RunCapabilityRemoteRunDBMySQLQuery, protocol.RunCapabilityRemoteRunDBSQLiteProbe, protocol.RunCapabilityRemoteRunDBSQLiteQuery, protocol.RunCapabilityRemoteRunLogsTransfer, protocol.RunCapabilityRemoteRunRCONCommand, protocol.RunCapabilityRemoteRunProtectedSQL, protocol.RunCapabilityRemoteRunProtectedRCON, protocol.RunCapabilityRemoteRunProgram, } } func (executor LifecycleExecutor) SupportedCapabilities() []string { return SupportedLifecycleCapabilities() } func (executor LifecycleExecutor) Execute(assignment protocol.RunJobAssignment) LifecycleExecutionResult { return executor.ExecuteContext(context.Background(), assignment) } func (executor LifecycleExecutor) ExecuteContext(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult { log.Printf("RUN phase=lifecycle status=starting job=%s capability=%s target=%s server=%s workspaceScope=%s", assignment.JobID, assignment.Capability, safeOptional(assignment.TargetKey), assignment.ServerInstanceID, safeOptional(assignment.ExecutionInput.WorkspaceScope)) if (assignment.Capability == protocol.RunCapabilityConfigWrite || assignment.Capability == protocol.RunCapabilityFilesList || assignment.Capability == protocol.RunCapabilityFilesRead || assignment.Capability == protocol.RunCapabilityFilesWrite) && assignment.ExecutionInput.WorkspaceScope != "" { if executor.fileExecutor == nil { log.Printf("RUN phase=lifecycle status=file_executor_unavailable job=%s", assignment.JobID) return lifecycleFailure("file_executor_unavailable", "file executor is unavailable") } log.Printf("RUN phase=lifecycle status=file_executor_start job=%s capability=%s", assignment.JobID, assignment.Capability) return executor.fileExecutor.Execute(ctx, assignment) } if !isSupportedLifecycleCapability(assignment.Capability) { log.Printf("RUN phase=lifecycle status=unsupported_capability job=%s capability=%s", assignment.JobID, assignment.Capability) return lifecycleFailure("unsupported_lifecycle_capability", "unsupported lifecycle capability") } if len(assignment.ExecutionInput.DLLExtensions) > 0 { log.Printf("RUN phase=lifecycle.dll status=validating job=%s extensions=%d", assignment.JobID, len(assignment.ExecutionInput.DLLExtensions)) if err := protocol.ValidateRunJobAssignment(assignment); err != nil { log.Printf("RUN phase=lifecycle.dll status=invalid job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("unsafe_dll_extension_plan", "DLL extension plan is invalid") } } if assignment.ExecutionInput.ServerDeploymentPlan != nil { log.Printf("RUN phase=lifecycle status=legacy_deployment_rejected job=%s", assignment.JobID) return lifecycleFailure("unsupported_legacy_deployment_plan", "game-specific deployment plans must be implemented by plugin lifecycle actions") } if assignment.ExecutionInput.Deployment != nil && assignment.ExecutionInput.Deployment.Mode == "custom-command" { log.Printf("RUN phase=lifecycle status=custom_deployment job=%s", assignment.JobID) return executor.executeDeployment(ctx, assignment) } log.Printf("RUN phase=lifecycle.template status=loading job=%s target=%s", assignment.JobID, safeOptional(assignment.TargetKey)) template, scope, err := executor.loadLifecycleTemplate(assignment) if err != nil { log.Printf("RUN phase=lifecycle.template status=failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("unsafe_lifecycle_command", err.Error()) } log.Printf("RUN phase=lifecycle.template status=loaded job=%s action=%s mode=%s scope=%s commandArgs=%d envKeys=%s", assignment.JobID, safeOptional(template.Action), safeOptional(template.Mode), scope, len(template.Command)+len(template.Arguments), envKeysSummary(template.Env, template.Environment)) if template.Action != "" { expectedAction := map[string]string{protocol.RunCapabilityProcessInstall: "install", protocol.RunCapabilityProcessStart: "start", protocol.RunCapabilityProcessStop: "stop", protocol.RunCapabilityProcessStatus: "status"}[assignment.Capability] if template.Action != expectedAction { log.Printf("RUN phase=lifecycle.template status=action_mismatch job=%s expected=%s actual=%s", assignment.JobID, expectedAction, template.Action) return lifecycleFailure("unsafe_lifecycle_command", "typed lifecycle action does not match capability") } if assignment.Capability == protocol.RunCapabilityProcessStart && template.Mode != "supervised" { log.Printf("RUN phase=lifecycle.template status=mode_mismatch job=%s expected=supervised actual=%s", assignment.JobID, template.Mode) return lifecycleFailure("unsafe_lifecycle_command", "typed start action must be supervised") } if (assignment.Capability == protocol.RunCapabilityProcessStop || assignment.Capability == protocol.RunCapabilityProcessStatus) && template.Mode != "control" { log.Printf("RUN phase=lifecycle.template status=mode_mismatch job=%s expected=control actual=%s", assignment.JobID, template.Mode) return lifecycleFailure("unsafe_lifecycle_command", "typed control action must use control mode") } } if assignment.Capability == protocol.RunCapabilityProcessStart && len(assignment.ExecutionInput.DLLExtensions) > 0 { log.Printf("RUN phase=lifecycle.dll status=synchronizing job=%s extensions=%d", assignment.JobID, len(assignment.ExecutionInput.DLLExtensions)) if err := executor.synchronizeUE4SSDLLExtensions(ctx, assignment, template, scope); err != nil { log.Printf("RUN phase=lifecycle.dll status=failed job=%s error=%s", assignment.JobID, err.Error()) return dllExtensionLifecycleFailure(err) } log.Printf("RUN phase=lifecycle.dll status=complete job=%s", assignment.JobID) } if executor.managed != nil && template.Action != "" && (assignment.Capability == protocol.RunCapabilityProcessStart || assignment.Capability == protocol.RunCapabilityProcessStop || assignment.Capability == protocol.RunCapabilityProcessStatus) { log.Printf("RUN phase=lifecycle.managed status=dispatch job=%s action=%s mode=%s", assignment.JobID, template.Action, template.Mode) return executor.executeManaged(ctx, assignment, template, scope) } log.Printf("RUN phase=lifecycle.command status=building job=%s", assignment.JobID) command, err := template.ToProcessCommand(scope, NewWorkspaceResolver(executor.workspaceRoot), assignment) if err != nil { log.Printf("RUN phase=lifecycle.command status=build_failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("unsafe_lifecycle_command", err.Error()) } log.Printf("RUN phase=lifecycle.command status=starting job=%s workdir=%s command=%s timeoutMs=%d envKeys=%s", assignment.JobID, safeOptional(command.WorkDir), quotedCommandLine(command.Args), command.Timeout.Milliseconds(), envKeysSummary(command.Env, nil)) command.OutputLine = func(stream string, line string) { _ = executor.logSink.Append(ctx, assignment, stream, line) } result, err := executor.supervisor.Run(ctx, command) if err != nil && ctx.Err() != nil { log.Printf("RUN phase=lifecycle.command status=cancelled job=%s error=%s", assignment.JobID, ctx.Err().Error()) return LifecycleExecutionResult{ State: lifecycleResultStateCancelled, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "lifecycle action cancelled"}, Message: "lifecycle action cancelled", ErrorCode: "lifecycle_cancelled", } } if !result.OutputRelayed { executor.writeProcessLogs(ctx, assignment, result) } if err != nil { log.Printf("RUN phase=lifecycle.command status=failed job=%s exitCode=%d error=%s", assignment.JobID, result.ExitCode, err.Error()) return lifecycleFailure("lifecycle_process_failed", err.Error()) } if result.ExitCode != 0 { log.Printf("RUN phase=lifecycle.command status=failed job=%s exitCode=%d", assignment.JobID, result.ExitCode) return lifecycleFailure("lifecycle_process_failed", fmt.Sprintf("lifecycle command exited with code %d", result.ExitCode)) } log.Printf("RUN phase=lifecycle.command status=exited job=%s exitCode=%d stdoutBytes=%d stderrBytes=%d", assignment.JobID, result.ExitCode, len(result.Stdout), len(result.Stderr)) artifactRef, err := executor.artifactHook.QueueLifecycleResult(ctx, assignment, result) if err != nil { log.Printf("RUN phase=lifecycle.artifact status=failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("lifecycle_artifact_hook_failed", err.Error()) } log.Printf("RUN phase=lifecycle status=succeeded job=%s resultRef=%s", assignment.JobID, safeOptional(artifactRef)) return LifecycleExecutionResult{ State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "lifecycle action completed"}, ResultRef: artifactRef, Message: fmt.Sprintf("%s completed", assignment.Capability), } } func (executor LifecycleExecutor) executeDeployment(ctx context.Context, assignment protocol.RunJobAssignment) LifecycleExecutionResult { definition := assignment.ExecutionInput.Deployment log.Printf("RUN phase=deployment status=starting job=%s mode=%s revision=%d", assignment.JobID, definition.Mode, definition.Revision) if definition.SchemaVersion != "1" || definition.Revision < 1 || definition.Mode == "" { log.Printf("RUN phase=deployment status=invalid job=%s", assignment.JobID) return lifecycleFailure("invalid_deployment_definition", "deployment definition is invalid") } action := map[string]string{protocol.RunCapabilityProcessInstall: "install", protocol.RunCapabilityProcessStart: "start", protocol.RunCapabilityProcessStop: "stop", protocol.RunCapabilityProcessStatus: "status"}[assignment.Capability] if action == "" { log.Printf("RUN phase=deployment status=invalid_action job=%s capability=%s", assignment.JobID, assignment.Capability) return lifecycleFailure("invalid_deployment_action", "deployment action is invalid") } commandText := map[string]string{"start": definition.StartCommand, "stop": definition.StopCommand, "status": definition.StatusCommand}[action] if action == "install" { commandText = definition.StartCommand } if commandText == "" { log.Printf("RUN phase=deployment status=missing_command job=%s action=%s", assignment.JobID, action) return lifecycleFailure("deployment_command_missing", "deployment command is not configured") } workdir := definition.WorkingDirectory if workdir == "" { workdir = definition.ServerRoot } if workdir == "" { log.Printf("RUN phase=deployment status=missing_workdir job=%s action=%s", assignment.JobID, action) return lifecycleFailure("deployment_workdir_missing", "deployment working directory is not configured") } args := strings.Fields(commandText) if len(args) == 0 { log.Printf("RUN phase=deployment status=invalid_command job=%s", assignment.JobID) return lifecycleFailure("deployment_command_invalid", "deployment command is invalid") } if definition.Shell != "" { log.Printf("RUN phase=deployment status=unsupported_shell job=%s shell=%s", assignment.JobID, definition.Shell) return lifecycleFailure("deployment_shell_unsupported", "deployment shell is not supported by this Run") } command := ProcessCommand{Args: args, WorkDir: workdir, JobID: assignment.JobID, Capability: assignment.Capability, Action: action} log.Printf("RUN phase=deployment.command status=starting job=%s action=%s revision=%d root=%s workdir=%s command=%s", assignment.JobID, action, definition.Revision, safeOptional(definition.ServerRoot), safeOptional(workdir), quotedCommandLine(command.Args)) result, err := executor.supervisor.Run(ctx, command) if err != nil { log.Printf("RUN phase=deployment.command status=failed job=%s exitCode=%d error=%s", assignment.JobID, result.ExitCode, err.Error()) return lifecycleFailure("lifecycle_process_failed", err.Error()) } executor.writeProcessLogs(ctx, assignment, result) if result.ExitCode != 0 { log.Printf("RUN phase=deployment.command status=failed job=%s exitCode=%d", assignment.JobID, result.ExitCode) return lifecycleFailure("lifecycle_process_failed", fmt.Sprintf("lifecycle command exited with code %d", result.ExitCode)) } log.Printf("RUN phase=deployment.command status=exited job=%s exitCode=%d stdoutBytes=%d stderrBytes=%d", assignment.JobID, result.ExitCode, len(result.Stdout), len(result.Stderr)) receipt := &protocol.ServerDeploymentExecutionReceipt{SchemaVersion: "1", Revision: definition.Revision, Action: action, Mode: definition.Mode, Shell: definition.Shell, UsedServerRoot: definition.ServerRoot != ""} log.Printf("RUN phase=deployment status=succeeded job=%s action=%s revision=%d", assignment.JobID, action, definition.Revision) return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "deployment lifecycle action completed"}, Message: fmt.Sprintf("%s completed", assignment.Capability), ExecutionResult: protocol.RunJobExecutionResult{Kind: "deployment.lifecycle", Summary: "deployment revision confirmed", DeploymentReceipt: receipt}} } func (executor LifecycleExecutor) ResolveCommand(assignment protocol.RunJobAssignment) (ProcessCommand, error) { workdir, err := scopedServerWorkspace(executor.workspaceRoot, assignment.ServerInstanceID) if err != nil { return ProcessCommand{}, err } if err := os.MkdirAll(workdir, 0o755); err != nil { return ProcessCommand{}, fmt.Errorf("create scoped workspace: %w", err) } template := LifecycleActionTemplate{ Command: []string{"true"}, TimeoutMS: int(defaultLifecycleTimeout / time.Millisecond), } if assignment.TargetKey != "" { path, err := scopedPath(workdir, assignment.TargetKey) if err != nil { return ProcessCommand{}, err } file, err := os.Open(path) if err != nil { return ProcessCommand{}, fmt.Errorf("open lifecycle action template: %w", err) } decodeErr := json.NewDecoder(file).Decode(&template) closeErr := file.Close() if decodeErr != nil { return ProcessCommand{}, fmt.Errorf("decode lifecycle action template: %w", decodeErr) } if closeErr != nil { return ProcessCommand{}, fmt.Errorf("close lifecycle action template: %w", closeErr) } } return template.ToProcessCommand(workdir, NewWorkspaceResolver(executor.workspaceRoot), assignment) } func (executor LifecycleExecutor) writeProcessLogs(ctx context.Context, assignment protocol.RunJobAssignment, result ProcessResult) { for _, item := range []struct { stream string body string }{ {stream: "stdout", body: result.Stdout}, {stream: "stderr", body: result.Stderr}, } { for _, line := range splitRawLogLines(item.body) { _ = executor.logSink.Append(ctx, assignment, item.stream, line) } } } type LifecycleActionTemplate struct { Version int `json:"version,omitempty"` Action string `json:"action,omitempty"` Mode string `json:"mode,omitempty"` ExecutableKey string `json:"executableKey,omitempty"` TargetExecutableKey string `json:"targetExecutableKey,omitempty"` Arguments []string `json:"arguments,omitempty"` Environment map[string]string `json:"environment,omitempty"` OutputMode string `json:"outputMode,omitempty"` StopTimeoutMS int `json:"stopTimeoutMs,omitempty"` Command []string `json:"command"` Env map[string]string `json:"env,omitempty"` TimeoutMS int `json:"timeoutMs,omitempty"` } func (executor LifecycleExecutor) loadLifecycleTemplate(assignment protocol.RunJobAssignment) (LifecycleActionTemplate, string, error) { scope, err := executor.lifecycleScope(assignment) if err != nil { return LifecycleActionTemplate{}, "", err } template := LifecycleActionTemplate{Command: []string{"true"}, TimeoutMS: int(defaultLifecycleTimeout / time.Millisecond)} if assignment.TargetKey == "" { if err := os.MkdirAll(scope, 0o755); err != nil { return LifecycleActionTemplate{}, "", fmt.Errorf("create lifecycle workspace: %w", err) } return template, scope, nil } path, err := NewWorkspaceResolver(executor.workspaceRoot).ExistingTarget(scope, assignment.TargetKey) if err != nil { return LifecycleActionTemplate{}, "", fmt.Errorf("open lifecycle action template: %w", err) } file, err := os.Open(path) if err != nil { return LifecycleActionTemplate{}, "", fmt.Errorf("open lifecycle action template: %w", err) } defer file.Close() decoder := json.NewDecoder(io.LimitReader(file, 16*1024)) decoder.DisallowUnknownFields() if err := decoder.Decode(&template); err != nil { return LifecycleActionTemplate{}, "", fmt.Errorf("decode lifecycle action template: %w", err) } return template, scope, nil } func (executor LifecycleExecutor) lifecycleScope(assignment protocol.RunJobAssignment) (string, error) { if assignment.ExecutionInput.WorkspaceScope != "" { return NewWorkspaceResolver(executor.workspaceRoot).Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope) } return scopedServerWorkspace(executor.workspaceRoot, assignment.ServerInstanceID) } func (executor LifecycleExecutor) executeManaged(ctx context.Context, assignment protocol.RunJobAssignment, template LifecycleActionTemplate, scope string) LifecycleExecutionResult { resolver := NewWorkspaceResolver(executor.workspaceRoot) identity := ProcessIdentity{Scope: scope, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, JobID: assignment.JobID, Capability: assignment.Capability, ProfileKey: assignment.ExecutionInput.WorkspaceScope, Attempt: assignment.Attempt, StdoutStreamKey: processLogStreamKey(assignment, "process.stdout", "stdout"), StderrStreamKey: processLogStreamKey(assignment, "process.stderr", "stderr")} if assignment.Capability == protocol.RunCapabilityProcessStart { log.Printf("RUN phase=lifecycle.managed status=building_start_command job=%s scope=%s", assignment.JobID, scope) command, err := template.ToManagedProcessCommand(resolver, scope, assignment) if err != nil { log.Printf("RUN phase=lifecycle.managed status=build_failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("unsafe_lifecycle_command", err.Error()) } log.Printf("RUN phase=lifecycle.managed status=starting job=%s workdir=%s command=%s timeoutMs=%d envKeys=%s", assignment.JobID, safeOptional(command.WorkDir), quotedCommandLine(command.Args), command.Timeout.Milliseconds(), envKeysSummary(command.Env, nil)) item, err := executor.managed.Start(ctx, command, identity, executor.managedProcessOutput(ctx, assignment)) if err != nil { if ctx.Err() != nil { log.Printf("RUN phase=lifecycle.managed status=cancelled job=%s error=%s", assignment.JobID, ctx.Err().Error()) return lifecycleExecutionFailure("lifecycle_cancelled", "lifecycle action cancelled", false) } log.Printf("RUN phase=lifecycle.managed status=failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("lifecycle_process_failed", err.Error()) } log.Printf("RUN phase=lifecycle.managed status=started job=%s pid=%d state=%s stdoutRef=%s stderrRef=%s", assignment.JobID, item.PID, item.State, safeOptional(item.StdoutLogRef), safeOptional(item.StderrLogRef)) return processExecutionResult(item, "process started") } if assignment.Capability == protocol.RunCapabilityProcessStop { log.Printf("RUN phase=lifecycle.managed status=stopping job=%s scope=%s", assignment.JobID, scope) item, err := executor.managed.Stop(ctx, identity) if err != nil { log.Printf("RUN phase=lifecycle.managed status=stop_failed job=%s error=%s", assignment.JobID, err.Error()) return lifecycleFailure("lifecycle_stop_failed", err.Error()) } log.Printf("RUN phase=lifecycle.managed status=stopped job=%s pid=%d state=%s classification=%s", assignment.JobID, item.PID, item.State, safeOptional(item.ExitClassification)) return processExecutionResult(item, "process stopped") } log.Printf("RUN phase=lifecycle.managed status=querying job=%s scope=%s", assignment.JobID, scope) item := executor.managed.Status(identity) log.Printf("RUN phase=lifecycle.managed status=queried job=%s pid=%d state=%s classification=%s", assignment.JobID, item.PID, item.State, safeOptional(item.ExitClassification)) return processExecutionResult(item, "process status queried") } func (executor LifecycleExecutor) ResumeManagedProcessLogs(ctx context.Context) { if executor.managed == nil { return } executor.managed.ResumeOutput(executor.managedProcessOutput(ctx, protocol.RunJobAssignment{})) } func (executor LifecycleExecutor) managedProcessOutput(ctx context.Context, assignment protocol.RunJobAssignment) ManagedProcessOutput { return ManagedProcessOutput{ Stdout: func(identity ProcessIdentity, line ManagedProcessLine) error { return executor.appendManagedProcessLog(ctx, assignment, identity, "stdout", line) }, Stderr: func(identity ProcessIdentity, line ManagedProcessLine) error { return executor.appendManagedProcessLog(ctx, assignment, identity, "stderr", line) }, } } func (executor LifecycleExecutor) appendManagedProcessLog(ctx context.Context, assignment protocol.RunJobAssignment, identity ProcessIdentity, stream string, line ManagedProcessLine) error { if executor.logSink == nil { return nil } if assignment.JobID == "" { assignment = assignmentFromProcessIdentity(identity) } assignment.LogSessionID = identity.LogSessionID assignment.SessionStartedAt = identity.StartedAt if assignment.JobID == "" || assignment.ServerInstanceID == "" { return nil } if sink, ok := executor.logSink.(ProcessLogCursorSink); ok { return sink.AppendWithCursor(ctx, assignment, stream, line.Text, ProcessLogCursor{StartOffset: line.StartOffset, EndOffset: line.EndOffset}) } return executor.logSink.Append(ctx, assignment, stream, line.Text) } func assignmentFromProcessIdentity(identity ProcessIdentity) protocol.RunJobAssignment { assignment := protocol.RunJobAssignment{ JobID: identity.JobID, ServerInstanceID: identity.ServerInstanceID, RunEndpointID: identity.RunEndpointID, Capability: identity.Capability, Attempt: identity.Attempt, LogSessionID: identity.LogSessionID, SessionStartedAt: identity.StartedAt, } if identity.StdoutStreamKey != "" { assignment.ExecutionInput.LogSources = append(assignment.ExecutionInput.LogSources, protocol.RuntimeLogSourcePlan{Key: "process-stdout", Kind: "process.stdout", StreamKey: identity.StdoutStreamKey, CursorKind: "sequence"}) } if identity.StderrStreamKey != "" { assignment.ExecutionInput.LogSources = append(assignment.ExecutionInput.LogSources, protocol.RuntimeLogSourcePlan{Key: "process-stderr", Kind: "process.stderr", StreamKey: identity.StderrStreamKey, CursorKind: "sequence"}) } return assignment } func processLogStreamKey(assignment protocol.RunJobAssignment, kind string, fallback string) string { for _, source := range assignment.ExecutionInput.LogSources { if source.Kind == kind && strings.TrimSpace(source.StreamKey) != "" { return source.StreamKey } } return fallback } func processExecutionResult(item ProcessIdentity, message string) LifecycleExecutionResult { state := item.State if state == "" { state = "stopped" } return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: message}, Message: message, ExecutionResult: protocol.RunJobExecutionResult{Kind: "process", ProcessState: state, ExitClassification: item.ExitClassification, ExitCode: item.ExitCode, Summary: "private supervised process identity"}} } func lifecycleExecutionFailure(code string, message string, retryable bool) LifecycleExecutionResult { return LifecycleExecutionResult{State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: message}, Message: message, ErrorCode: code, Retryable: retryable, ExecutionResult: protocol.RunJobExecutionResult{Kind: "file", Summary: code}} } func (template LifecycleActionTemplate) ToProcessCommand(workdir string, resolver WorkspaceResolver, assignment protocol.RunJobAssignment) (ProcessCommand, error) { if template.ExecutableKey != "" { return template.ToExecutableProcessCommand(resolver, workdir, assignment) } args := append([]string(nil), template.Command...) if len(args) == 0 { return ProcessCommand{}, fmt.Errorf("command is required") } for i, part := range args { if strings.TrimSpace(part) == "" { return ProcessCommand{}, fmt.Errorf("command part is required") } if containsUnsafeRuntimeText(part) { return ProcessCommand{}, fmt.Errorf("command contains unsafe content") } if i == 0 { if !commandNamePattern.MatchString(part) || strings.Contains(part, "/") || filepath.IsAbs(part) { return ProcessCommand{}, fmt.Errorf("command executable must be an allowlisted name") } if _, disallowed := disallowedExecutables[strings.ToLower(part)]; disallowed { return ProcessCommand{}, fmt.Errorf("command executable must not be a shell") } continue } if strings.ContainsAny(part, "|;&`$<>") { return ProcessCommand{}, fmt.Errorf("command arguments must not contain shell metacharacters") } } env, err := template.executionEnvironment(assignment) if err != nil { return ProcessCommand{}, err } timeout := defaultLifecycleTimeout if template.TimeoutMS > 0 { timeout = time.Duration(template.TimeoutMS) * time.Millisecond } if timeout > maxLifecycleTimeout { return ProcessCommand{}, fmt.Errorf("timeout is too large") } return ProcessCommand{WorkDir: workdir, Args: args, Env: env, Timeout: timeout, JobID: assignment.JobID, Capability: assignment.Capability, Action: template.Action}, nil } func (template LifecycleActionTemplate) ToExecutableProcessCommand(resolver WorkspaceResolver, scope string, assignment protocol.RunJobAssignment) (ProcessCommand, error) { if template.ExecutableKey == "" { return ProcessCommand{}, fmt.Errorf("typed executableKey is required") } executable, err := resolver.ExistingTarget(scope, template.ExecutableKey) if err != nil { return ProcessCommand{}, err } info, err := os.Stat(executable) if err != nil || !info.Mode().IsRegular() || runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 { return ProcessCommand{}, fmt.Errorf("typed executable is not executable") } args := append([]string{executable}, template.Arguments...) for _, part := range args[1:] { if strings.TrimSpace(part) == "" || containsUnsafeRuntimeText(part) || strings.ContainsAny(part, "|;&`$<>") { return ProcessCommand{}, fmt.Errorf("typed argument is unsafe") } } if template.OutputMode != "" && template.OutputMode != "pipes" && template.OutputMode != "console" { return ProcessCommand{}, fmt.Errorf("typed output mode is unsupported") } env, err := template.executionEnvironment(assignment) if err != nil { return ProcessCommand{}, err } timeout := defaultLifecycleTimeout if template.TimeoutMS > 0 { timeout = time.Duration(template.TimeoutMS) * time.Millisecond } if timeout > maxLifecycleTimeout { return ProcessCommand{}, fmt.Errorf("timeout is too large") } args = managedExecutableArgs(runtime.GOOS, args) return ProcessCommand{WorkDir: scope, Args: args, Env: env, OutputMode: template.OutputMode, Timeout: timeout, JobID: assignment.JobID, Capability: assignment.Capability, Action: template.Action}, nil } func managedExecutableArgs(targetOS string, args []string) []string { if targetOS != "windows" || len(args) == 0 { return args } ext := strings.ToLower(filepath.Ext(args[0])) if ext != ".cmd" && ext != ".bat" { return args } // Keep the batch path as a normal /c argument. Passing a pre-quoted // command string here makes ComposeCommandLine escape the inner quotes // for CreateProcess; cmd.exe does not treat those backslashes as quote // escapes and consequently fails before it can run the script. return append([]string{"cmd.exe", "/d", "/c", "call"}, args...) } func quoteWindowsCommandArg(value string) string { if value != "" && !strings.ContainsAny(value, " \t\"") { return value } var builder strings.Builder builder.WriteByte('"') backslashes := 0 for _, char := range value { if char == '\\' { backslashes++ continue } if char == '"' { builder.WriteString(strings.Repeat("\\", backslashes*2+1)) builder.WriteRune(char) backslashes = 0 continue } if backslashes > 0 { builder.WriteString(strings.Repeat("\\", backslashes)) backslashes = 0 } builder.WriteRune(char) } if backslashes > 0 { builder.WriteString(strings.Repeat("\\", backslashes*2)) } builder.WriteByte('"') return builder.String() } func (template LifecycleActionTemplate) ToManagedProcessCommand(resolver WorkspaceResolver, scope string, assignment protocol.RunJobAssignment) (ProcessCommand, error) { command, err := template.ToExecutableProcessCommand(resolver, scope, assignment) if err != nil { return ProcessCommand{}, err } return command, nil } func (template LifecycleActionTemplate) executionEnvironment(assignment protocol.RunJobAssignment) (map[string]string, error) { env := template.Environment if env == nil { env = template.Env } validated := make(map[string]string, len(env)+16) for key, value := range env { if !envNamePattern.MatchString(key) || (!strings.HasPrefix(key, "GAME_") && !strings.HasPrefix(key, "SERVER_") && !strings.HasPrefix(key, "RUN_")) || containsUnsafeRuntimeText(value) { return nil, fmt.Errorf("typed environment is unsafe") } validated[key] = value } add := func(key string, value string) error { if strings.TrimSpace(value) == "" { return nil } if !envNamePattern.MatchString(key) || (!strings.HasPrefix(key, "GAME_") && !strings.HasPrefix(key, "SERVER_") && !strings.HasPrefix(key, "RUN_")) || containsUnsafeRuntimeText(value) { return fmt.Errorf("typed environment is unsafe") } validated[key] = value return nil } if err := add("SERVER_PLUGIN_ID", assignment.ExecutionInput.PluginID); err != nil { return nil, err } if err := add("SERVER_LIFECYCLE_OPERATION", assignment.ExecutionInput.LifecycleOperation); err != nil { return nil, err } if deployment := assignment.ExecutionInput.Deployment; deployment != nil { if err := add("SERVER_DEPLOYMENT_MODE", deployment.Mode); err != nil { return nil, err } if err := add("SERVER_PROFILE_KEY", deployment.ProfileKey); err != nil { return nil, err } if err := add("SERVER_ROOT", deployment.ServerRoot); err != nil { return nil, err } if err := add("SERVER_WORKING_DIRECTORY", deployment.WorkingDirectory); err != nil { return nil, err } if deployment.Revision > 0 { if err := add("SERVER_REVISION", fmt.Sprint(deployment.Revision)); err != nil { return nil, err } } for key, value := range deployment.CreateInputs { suffix := envKeySuffix(key) if suffix == "" { return nil, fmt.Errorf("typed environment is unsafe") } if err := add("SERVER_CREATE_"+suffix, value); err != nil { return nil, err } } } for key, value := range assignment.ExecutionInput.Inputs { suffix := envKeySuffix(key) if suffix == "" { return nil, fmt.Errorf("typed environment is unsafe") } if err := add("SERVER_INPUT_"+suffix, value); err != nil { return nil, err } } return validated, nil } func envKeySuffix(key string) string { var builder strings.Builder for _, char := range key { switch { case char >= 'a' && char <= 'z': builder.WriteRune(char - 'a' + 'A') case char >= 'A' && char <= 'Z': builder.WriteRune(char) case char >= '0' && char <= '9': builder.WriteRune(char) case char == '_' || char == '-' || char == '.' || char == '/': builder.WriteByte('_') default: return "" } } return strings.Trim(builder.String(), "_") } type ProcessCommand struct { WorkDir string Args []string Env map[string]string OutputMode string Timeout time.Duration JobID string Capability string Action string OutputLine func(string, string) `json:"-"` } type ProcessResult struct { ExitCode int Stdout string Stderr string OutputRelayed bool } type ProcessSupervisor interface { Run(context.Context, ProcessCommand) (ProcessResult, error) } type OSProcessSupervisor struct{} func (supervisor OSProcessSupervisor) Run(ctx context.Context, command ProcessCommand) (ProcessResult, error) { if len(command.Args) == 0 { log.Printf("RUN phase=process.command status=missing_executable job=%s capability=%s action=%s", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action)) return ProcessResult{ExitCode: -1}, fmt.Errorf("command is required") } startedAt := time.Now() log.Printf("RUN phase=process.command status=starting job=%s capability=%s action=%s workdir=%s command=%s timeoutMs=%d envKeys=%s", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action), safeOptional(command.WorkDir), quotedCommandLine(command.Args), command.Timeout.Milliseconds(), envKeysSummary(command.Env, nil)) if command.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, command.Timeout) defer cancel() } cmd := exec.CommandContext(ctx, command.Args[0], command.Args[1:]...) cmd.Dir = command.WorkDir cmd.Env = os.Environ() for key, value := range command.Env { cmd.Env = append(cmd.Env, key+"="+value) } var stdout bytes.Buffer var stderr bytes.Buffer stdoutWriter := newLifecycleOutputWriter(&stdout, maxLifecycleOutputBytes, command, "stdout") stderrWriter := newLifecycleOutputWriter(&stderr, maxLifecycleOutputBytes, command, "stderr") cmd.Stdout = stdoutWriter cmd.Stderr = stderrWriter if err := cmd.Start(); err != nil { log.Printf("RUN phase=process.command status=start_failed job=%s capability=%s action=%s command=%s durationMs=%d error=%s", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action), quotedCommandLine(command.Args), time.Since(startedAt).Milliseconds(), err.Error()) return ProcessResult{ExitCode: -1}, err } pid := 0 if cmd.Process != nil { pid = cmd.Process.Pid } log.Printf("RUN phase=process.command status=started job=%s capability=%s action=%s pid=%d command=%s", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action), pid, quotedCommandLine(command.Args)) err := cmd.Wait() stdoutWriter.Flush() stderrWriter.Flush() result := ProcessResult{Stdout: stdout.String(), Stderr: stderr.String(), OutputRelayed: command.OutputLine != nil} if cmd.ProcessState != nil { result.ExitCode = cmd.ProcessState.ExitCode() } if err != nil { log.Printf("RUN phase=process.command status=failed job=%s capability=%s action=%s pid=%d command=%s exitCode=%d durationMs=%d stdoutBytes=%d stderrBytes=%d error=%s", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action), pid, quotedCommandLine(command.Args), result.ExitCode, time.Since(startedAt).Milliseconds(), len(result.Stdout), len(result.Stderr), err.Error()) return result, err } log.Printf("RUN phase=process.command status=exited job=%s capability=%s action=%s pid=%d command=%s exitCode=%d durationMs=%d stdoutBytes=%d stderrBytes=%d", safeOptional(command.JobID), safeOptional(command.Capability), safeOptional(command.Action), pid, quotedCommandLine(command.Args), result.ExitCode, time.Since(startedAt).Milliseconds(), len(result.Stdout), len(result.Stderr)) return result, nil } type lifecycleOutputWriter struct { mu sync.Mutex buffer *bytes.Buffer limit int command ProcessCommand stream string pending string } func newLifecycleOutputWriter(buffer *bytes.Buffer, limit int, command ProcessCommand, stream string) *lifecycleOutputWriter { return &lifecycleOutputWriter{buffer: buffer, limit: limit, command: command, stream: stream} } func (writer *lifecycleOutputWriter) Write(p []byte) (int, error) { writer.mu.Lock() defer writer.mu.Unlock() remaining := writer.limit - writer.buffer.Len() if remaining > 0 { if len(p) > remaining { _, _ = writer.buffer.Write(p[:remaining]) } else { _, _ = writer.buffer.Write(p) } } writer.pending += string(p) for { index := strings.IndexByte(writer.pending, '\n') if index < 0 { break } line := writer.pending[:index] writer.pending = writer.pending[index+1:] writer.logLine(line) } return len(p), nil } func (writer *lifecycleOutputWriter) Flush() { writer.mu.Lock() defer writer.mu.Unlock() if writer.pending == "" { return } writer.logLine(writer.pending) writer.pending = "" } func (writer *lifecycleOutputWriter) logLine(line string) { if writer.command.OutputLine != nil { writer.command.OutputLine(writer.stream, line) } } type ioLimitWriter struct { Writer *bytes.Buffer Limit int } func (writer ioLimitWriter) Write(p []byte) (int, error) { remaining := writer.Limit - writer.Writer.Len() if remaining > 0 { if len(p) > remaining { _, _ = writer.Writer.Write(p[:remaining]) } else { _, _ = writer.Writer.Write(p) } } return len(p), nil } type ProcessLogSink interface { Append(context.Context, protocol.RunJobAssignment, string, string) error } type ProcessLogCursor struct { StartOffset int64 EndOffset int64 } type ProcessLogCursorSink interface { AppendWithCursor(context.Context, protocol.RunJobAssignment, string, string, ProcessLogCursor) error } type NoopProcessLogSink struct{} func (NoopProcessLogSink) Append(context.Context, protocol.RunJobAssignment, string, string) error { return nil } type LifecycleArtifactHook interface { QueueLifecycleResult(context.Context, protocol.RunJobAssignment, ProcessResult) (string, error) } type StaticLifecycleArtifactHook struct{} func (StaticLifecycleArtifactHook) QueueLifecycleResult(_ context.Context, assignment protocol.RunJobAssignment, _ ProcessResult) (string, error) { return fmt.Sprintf("artifact://jobs/%s/lifecycle-result", url.PathEscape(assignment.JobID)), nil } func LifecycleResultRequest(assignment protocol.RunJobAssignment, sessionToken string, result LifecycleExecutionResult) protocol.RunJobResultRequest { return protocol.RunJobResultRequest{ RunEndpointID: assignment.RunEndpointID, SessionToken: sessionToken, JobID: assignment.JobID, LeaseToken: assignment.LeaseToken, Attempt: assignment.Attempt, State: result.State, Progress: result.Progress, ResultRef: result.ResultRef, Message: result.Message, ErrorCode: result.ErrorCode, Retryable: result.Retryable, ExecutionResult: protocol.RunJobExecutionResult{Kind: result.ExecutionResult.Kind, ProcessState: result.ExecutionResult.ProcessState, ExitClassification: result.ExecutionResult.ExitClassification, ExitCode: result.ExecutionResult.ExitCode, Version: result.ExecutionResult.Version, Checksum: result.ExecutionResult.Checksum, SizeBytes: result.ExecutionResult.SizeBytes, Summary: result.ExecutionResult.Summary, Content: result.ExecutionResult.Content, SQLiteSchemaProbe: result.ExecutionResult.SQLiteSchemaProbe, DeploymentReceipt: result.ExecutionResult.DeploymentReceipt, ServerDeploymentEvidence: result.ExecutionResult.ServerDeploymentEvidence}, } } func isSupportedLifecycleCapability(capability string) bool { for _, supported := range SupportedLifecycleCapabilities() { if capability == supported { return true } } return false } func isSupportedRemoteCapability(capability string) bool { for _, supported := range SupportedRemoteCapabilities() { if capability == supported { return true } } return false } func lifecycleFailure(code string, message string) LifecycleExecutionResult { return LifecycleExecutionResult{ State: lifecycleResultStateFailed, Progress: protocol.RunJobProgressReport{Percent: 100, Message: message}, Message: message, ErrorCode: code, } } func scopedServerWorkspace(root string, serverInstanceID string) (string, error) { if strings.TrimSpace(serverInstanceID) == "" { return "", fmt.Errorf("server instance id is required") } if containsUnsafeRuntimeText(serverInstanceID) || strings.ContainsAny(serverInstanceID, `/\`) || serverInstanceID == "." || serverInstanceID == ".." { return "", fmt.Errorf("server instance id is unsafe") } return scopedPath(root, serverInstanceID) } func scopedPath(root string, key string) (string, error) { if strings.TrimSpace(root) == "" { return "", fmt.Errorf("workspace root is required") } if strings.TrimSpace(key) == "" { return "", fmt.Errorf("logical key is required") } if filepath.IsAbs(key) || strings.Contains(key, "..") || strings.Contains(key, `\`) || containsUnsafeRuntimeText(key) { return "", fmt.Errorf("logical key is unsafe") } cleanRoot, err := filepath.Abs(root) if err != nil { return "", err } candidate := filepath.Clean(filepath.Join(cleanRoot, filepath.FromSlash(key))) rel, err := filepath.Rel(cleanRoot, candidate) if err != nil { return "", err } if rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) { return "", fmt.Errorf("logical key escapes workspace") } return candidate, nil } func containsUnsafeRuntimeText(value string) bool { normalized := strings.ToLower(value) for _, marker := range []string{"/users/", "/.ssh/", "password=", "apikey", "api_key", "secret=", "bearer ", "sk-", "unix://", "tcp://", "://"} { if strings.Contains(normalized, marker) { return true } } return false } func safeOptional(value string) string { value = strings.TrimSpace(value) if value == "" { return "-" } return value } func errorSummary(err error) string { if err == nil { return "-" } return err.Error() } func quotedCommandLine(args []string) string { if len(args) == 0 { return "-" } parts := make([]string, len(args)) for i, arg := range args { parts[i] = strconv.Quote(arg) } return strings.Join(parts, " ") } func envKeysSummary(first map[string]string, second map[string]string) string { seen := map[string]struct{}{} for key := range first { seen[key] = struct{}{} } for key := range second { seen[key] = struct{}{} } if len(seen) == 0 { return "-" } keys := make([]string, 0, len(seen)) for key := range seen { keys = append(keys, key) } sort.Strings(keys) return strings.Join(keys, ",") } // splitRawLogLines only removes the newline framing used by LogEntry. It // deliberately preserves every other byte, including blank lines and spaces. func splitRawLogLines(value string) []string { if value == "" { return nil } lines := make([]string, 0, strings.Count(value, "\n")+1) for start := 0; start < len(value); { end := strings.IndexByte(value[start:], '\n') if end < 0 { lines = append(lines, value[start:]) break } end += start lines = append(lines, value[start:end]) start = end + 1 } return lines } func checksumForText(value string) string { sum := sha256.Sum256([]byte(value)) return "sha256:" + hex.EncodeToString(sum[:]) }