package service import ( "fmt" "strings" "browser.local/platform/domain" "browser.local/platform/validator" ) func (svc *CoreService) ListRemoteAdapterDeclarationsForSession(sessionID string, serverInstanceID string) ([]domain.RemoteAdapterDeclaration, error) { instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID) if err != nil { return nil, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return nil, err } endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID) if err != nil { return nil, err } if !plugin.Permissions.RemoteAccess { return []domain.RemoteAdapterDeclaration{}, nil } declarations := make([]domain.RemoteAdapterDeclaration, 0, len(plugin.RuntimeProfiles.TransportProfiles)) for _, profile := range plugin.RuntimeProfiles.TransportProfiles { capabilities := intersectRemoteCapabilities(profile.Capabilities, plugin.RemoteAccess.RunCapabilities, endpoint.Capabilities) if len(capabilities) == 0 || strings.TrimSpace(profile.TargetKey) == "" { continue } declaration := domain.RemoteAdapterDeclaration{Key: profile.Key, Kind: remoteAdapterKind(profile.Kind), TargetKeys: []string{profile.TargetKey}, Capabilities: capabilities, TimeoutSeconds: 30, MaxAttempts: 3} if err := validator.ValidateRemoteAdapterDeclaration(declaration); err != nil { return nil, err } declarations = append(declarations, declaration) } return domain.CopyRemoteAdapterDeclarations(declarations), nil } func (svc *CoreService) RequestRemoteAdapterForSession(sessionID string, request domain.RemoteAdapterRequest) (domain.RemoteAdapterResult, error) { request = domain.CopyRemoteAdapterRequest(request) if err := validator.ValidateRemoteAdapterRequest(request); err != nil { return domain.RemoteAdapterResult{}, err } instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID) if err != nil { return domain.RemoteAdapterResult{}, err } declarations, err := svc.ListRemoteAdapterDeclarationsForSession(sessionID, instance.ID) if err != nil { return domain.RemoteAdapterResult{}, err } var selected domain.RemoteAdapterDeclaration for _, declaration := range declarations { if declaration.Key == request.DeclarationKey && containsString(declaration.TargetKeys, request.TargetKey) && containsString(declaration.Capabilities, request.Capability) { selected = declaration break } } if selected.Key == "" { plugin, pluginErr := svc.store.GamePlugins().Get(instance.PluginID) endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID) if pluginErr == nil && endpointErr == nil && plugin.Permissions.RemoteAccess && containsString(plugin.RemoteAccess.RunCapabilities, request.Capability) && containsString(endpoint.Capabilities, request.Capability) { selected = domain.RemoteAdapterDeclaration{Key: "legacy-" + string(remoteAdapterKindForCapability(request.Capability)), Kind: remoteAdapterKindForCapability(request.Capability), TargetKeys: []string{request.TargetKey}, Capabilities: []string{request.Capability}, TimeoutSeconds: 30, MaxAttempts: 3} } if selected.Key == "" { user, _ := svc.GetCurrentUser(sessionID) _ = svc.recordAuditEvent(user.ID, "remote-adapter.authorize", "server-instance", instance.ID, domain.AuditResultDenied, "remote adapter declaration, target, or capability was not approved") return domain.RemoteAdapterResult{}, ErrForbidden } } timeout := request.TimeoutSeconds if timeout == 0 { timeout = selected.TimeoutSeconds } attempts := request.MaxAttempts if attempts == 0 { attempts = selected.MaxAttempts } if timeout > selected.TimeoutSeconds || attempts > selected.MaxAttempts { return domain.RemoteAdapterResult{}, validationError("remote adapter timeout or retry exceeds declaration") } inputRef := request.InputRef if inputRef == "" { inputRef = fmt.Sprintf("input://remote-adapters/%s/%s", instance.ID, request.DeclarationKey) } job := domain.Job{ ID: jobIDFromParts("job-remote-adapter", instance.ID, request.IdempotencyKey), ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: request.Capability, TargetKey: request.TargetKey, InputRef: inputRef, IdempotencyKey: request.IdempotencyKey, Progress: domain.JobProgress{Percent: 0, Message: "scoped remote adapter queued"}, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: attempts, InitialBackoffSeconds: 2, MaxBackoffSeconds: 30}, ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), RemoteAdapterKey: selected.Key, RemoteAdapterKind: string(selected.Kind), TimeoutSeconds: timeout, Inputs: domain.CopyStringMap(request.Inputs)}, } created, err := svc.CreateJob(job) if err != nil { return domain.RemoteAdapterResult{}, err } user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.RemoteAdapterResult{}, err } auditID, err := svc.recordAuditEventWithID(user.ID, "remote-adapter.authorize", "server-instance", instance.ID, domain.AuditResultQueued, "authorized declared remote adapter target with bounded timeout and retry") if err != nil { return domain.RemoteAdapterResult{}, err } return domain.RemoteAdapterResult{RequestID: created.ID, ServerInstanceID: instance.ID, DeclarationKey: selected.Key, TargetKey: request.TargetKey, Kind: selected.Kind, Status: string(created.State), Retryable: attempts > 1, Message: "scoped remote adapter queued", ResultRef: "job://" + created.ID, AuditEventID: auditID}, nil } func intersectRemoteCapabilities(profile []string, declared []string, endpoint []string) []string { result := make([]string, 0, len(profile)) for _, capability := range profile { if isRemoteAdapterCapability(capability) && containsString(declared, capability) && containsString(endpoint, capability) { result = append(result, capability) } } return result } func isRemoteAdapterCapability(capability string) bool { switch capability { case domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite, domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite, domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite, domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop, domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand: return true default: return false } } func remoteAdapterKind(kind string) domain.RemoteAdapterKind { switch strings.ToLower(strings.TrimSpace(kind)) { case "ftp": return domain.RemoteAdapterFTP case "rsync": return domain.RemoteAdapterRsync case "file": return domain.RemoteAdapterRunFile case "process": return domain.RemoteAdapterRunProcess case "sqlite", "mysql", "database": return domain.RemoteAdapterDatabase case "rcon": return domain.RemoteAdapterRCON default: return domain.RemoteAdapterKind(strings.ToLower(strings.TrimSpace(kind))) } } func remoteAdapterKindForCapability(capability string) domain.RemoteAdapterKind { switch capability { case domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite: return domain.RemoteAdapterFTP case domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite: return domain.RemoteAdapterRsync case domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite: return domain.RemoteAdapterRunFile case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop: return domain.RemoteAdapterRunProcess case domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery: return domain.RemoteAdapterDatabase case domain.JobCapabilityRemoteRunRCONCommand: return domain.RemoteAdapterRCON case domain.JobCapabilityRemoteRunLogsTransfer: return domain.RemoteAdapterKind("log-transfer") default: return "" } }