Files
Unraid-AppOps-Gateway-Public/backend/app/services/action_registry.py
T

631 lines
21 KiB
Python

from __future__ import annotations
ACTIONS = [
'container.status',
'container.logs',
'container.clear_logs',
'container.restart',
'container.stop',
'runner.fleet.upgrade_v2',
'runner.fleet.specialize',
'runner.statistics.configure',
'controller.artifact.promote',
'controller.managed-storage.initialize',
'hub.migration.prepare_permissions',
'gateway.clear_cache',
'stack.status',
'stack.redeploy',
'deployment.preflight',
'deployment.targets.sync',
'deployment.runnerless.provision',
'deployment.target.deploy_workmesh',
'deployment.target.promote_workmesh',
'deployment.target.register_portfolio',
'deployment.target.register_privatemuse',
'deployment.target.reconcile_media_codec_dashboard',
'deployment.webhook.repair_privatemuse',
'privatemuse.comfyui.install_artifact',
'deployment.apply',
'deployment.rollback',
'volume.delete',
'unraid.health_summary',
'unraid.vms',
'unraid.vm.start',
'unraid.vm.resume',
'unraid.vm.pause',
'unraid.vm.stop',
'unraid.vm.reboot',
'unraid.vm.force_stop',
'unraid.vm.reset',
'unraid.mover.start',
'setup.status',
'safety.summary',
'media_pipeline.check',
'media_pipeline.diagnose',
'notifications.summary',
'notifications.evaluate_safety',
'sonarr.search_series',
'sonarr.add_series',
'sonarr.health',
'sonarr.queue_status',
'sonarr.wanted_missing',
'sonarr.delete_series',
'sonarr.mass_edit',
'radarr.search_movie',
'radarr.add_movie',
'radarr.health',
'radarr.queue_status',
'radarr.wanted_missing',
'radarr.delete_movie',
'radarr.delete_files',
'radarr.mass_edit',
'jellyfin.scan_library',
'widefrog.health',
'widefrog.queue_status',
'widefrog.failed_jobs',
'widefrog.activity',
'widefrog.retry_failed',
'widefrog.scan_library',
'widefrog.download_status',
'prowlarr.health',
'prowlarr.indexer_status',
'prowlarr.application_status',
'prowlarr.indexer_health',
'prowlarr.search_test',
'prowlarr.sync_status',
'prowlarr.delete_indexer',
'prowlarr.delete_application',
'prowlarr.update_indexer',
'prowlarr.update_application',
'prowlarr.grab_release',
'prowlarr.push_release',
'prowlarr.mass_edit',
'prowlarr.test_all_indexers',
'deluge.health',
'deluge.session_status',
'deluge.torrent_status',
'deluge.active_downloads',
'deluge.stalled_downloads',
'deluge.paused_downloads',
'deluge.speed_status',
'deluge.pause_torrent',
'deluge.resume_torrent',
'deluge.pause_all',
'deluge.resume_all',
'deluge.remove_torrent',
'deluge.remove_torrent_data',
'deluge.delete_data',
'deluge.force_recheck',
'deluge.move_storage',
'deluge.add_torrent',
'deluge.add_magnet',
'deluge.mass_edit',
'deluge.set_options',
'deluge.unknown_mutation',
'plex.health',
'plex.server_status',
'plex.library_status',
'plex.active_sessions',
'plex.recent_media',
'plex.scan_library',
'plex.scan_all_libraries',
'plex.stream_summary',
'tautulli.health',
'tautulli.status',
'tautulli.activity',
'tautulli.history',
'tautulli.users',
'tautulli.libraries',
'tautulli.recently_added',
'tautulli.watch_stats',
'tautulli.stream_summary',
'tautulli.plex_info',
'npm.health',
'npm.proxy_hosts',
'npm.certificates',
'npm.expiring_certificates',
'npm.access_lists',
'npm.exposure_summary',
'npm.host_reachability',
'npm.security_warnings',
'npm.create_proxy_host',
'npm.update_proxy_host',
'npm.delete_proxy_host',
'npm.enable_proxy_host',
'npm.disable_proxy_host',
'npm.renew_certificate',
'npm.delete_certificate',
'npm.modify_access_list',
'npm.create_user',
'npm.update_user',
'npm.delete_user',
'npm.mass_edit',
'npm.unknown_mutation',
'authentik.health',
'authentik.system_status',
'authentik.applications',
'authentik.providers',
'authentik.proxy_providers',
'authentik.oauth2_providers',
'authentik.outposts',
'authentik.outpost_health',
'authentik.flows_summary',
'authentik.policies_summary',
'authentik.events',
'authentik.auth_coverage',
'authentik.security_warnings',
'authentik.create_application',
'authentik.update_application',
'authentik.delete_application',
'authentik.create_provider',
'authentik.update_provider',
'authentik.delete_provider',
'authentik.create_outpost',
'authentik.update_outpost',
'authentik.delete_outpost',
'authentik.create_policy',
'authentik.update_policy',
'authentik.delete_policy',
'authentik.create_user',
'authentik.update_user',
'authentik.delete_user',
'authentik.create_group',
'authentik.update_group',
'authentik.delete_group',
'authentik.rotate_token',
'authentik.force_sync',
'authentik.mass_edit',
'authentik.unknown_mutation',
'tautulli.terminate_session',
'tautulli.delete_newsletter',
'tautulli.delete_notification_log',
'tautulli.delete_notifier',
'tautulli.delete_recently_added',
'tautulli.download_config',
'tautulli.download_database',
'tautulli.download_export',
'tautulli.download_log',
'tautulli.download_plex_log',
'tautulli.notify',
'tautulli.user_management',
'tautulli.mass_edit',
'tautulli.unknown_mutation',
'plex.delete_media',
'plex.delete_library',
'plex.empty_trash',
'plex.clean_bundles',
'plex.optimize_database',
'plex.kill_session',
'plex.user_management',
'plex.force_refresh_all_metadata',
'plex.mass_edit',
'plex.unknown_mutation',
'widefrog.delete_job',
'widefrog.delete_media',
'widefrog.clear_library',
'widefrog.clear_queue',
'widefrog.reset_database',
'widefrog.delete_downloads',
'widefrog.prune',
'playbook.run',
]
ACTION_RISK = {
'container.status': 'low',
'container.logs': 'low',
'container.clear_logs': 'high',
'container.restart': 'medium',
'container.stop': 'high',
'runner.fleet.upgrade_v2': 'high',
'runner.fleet.specialize': 'high',
'runner.statistics.configure': 'high',
'controller.artifact.promote': 'high',
'controller.managed-storage.initialize': 'high',
'hub.migration.prepare_permissions': 'high',
'gateway.clear_cache': 'high',
'stack.status': 'low',
'stack.redeploy': 'high',
'deployment.preflight': 'low',
'deployment.targets.sync': 'high',
'deployment.runnerless.provision': 'high',
'deployment.target.deploy_workmesh': 'high',
'deployment.target.promote_workmesh': 'high',
'deployment.target.register_portfolio': 'high',
'deployment.target.register_privatemuse': 'high',
'deployment.target.reconcile_media_codec_dashboard': 'high',
'deployment.webhook.repair_privatemuse': 'high',
'privatemuse.comfyui.install_artifact': 'high',
'deployment.apply': 'high',
'deployment.rollback': 'high',
'volume.delete': 'critical',
'unraid.health_summary': 'low',
'unraid.vms': 'low',
'unraid.vm.start': 'medium',
'unraid.vm.resume': 'medium',
'unraid.vm.pause': 'high',
'unraid.vm.stop': 'critical',
'unraid.vm.reboot': 'critical',
'unraid.vm.force_stop': 'critical',
'unraid.vm.reset': 'critical',
'unraid.mover.start': 'high',
'setup.status': 'low',
'safety.summary': 'low',
'media_pipeline.check': 'low',
'media_pipeline.diagnose': 'low',
'notifications.summary': 'low',
'notifications.evaluate_safety': 'medium',
'sonarr.search_series': 'low',
'sonarr.add_series': 'low',
'sonarr.health': 'low',
'sonarr.queue_status': 'low',
'sonarr.wanted_missing': 'low',
'sonarr.delete_series': 'critical',
'sonarr.mass_edit': 'critical',
'radarr.search_movie': 'low',
'radarr.add_movie': 'low',
'radarr.health': 'low',
'radarr.queue_status': 'low',
'radarr.wanted_missing': 'low',
'radarr.delete_movie': 'critical',
'radarr.delete_files': 'critical',
'radarr.mass_edit': 'critical',
'jellyfin.scan_library': 'medium',
'widefrog.health': 'low',
'widefrog.queue_status': 'low',
'widefrog.failed_jobs': 'low',
'widefrog.activity': 'low',
'widefrog.retry_failed': 'medium',
'widefrog.scan_library': 'medium',
'widefrog.download_status': 'low',
'prowlarr.health': 'low',
'prowlarr.indexer_status': 'low',
'prowlarr.application_status': 'low',
'prowlarr.indexer_health': 'low',
'prowlarr.search_test': 'medium',
'prowlarr.sync_status': 'low',
'prowlarr.delete_indexer': 'critical',
'prowlarr.delete_application': 'critical',
'prowlarr.update_indexer': 'high',
'prowlarr.update_application': 'high',
'prowlarr.grab_release': 'critical',
'prowlarr.push_release': 'critical',
'prowlarr.mass_edit': 'critical',
'prowlarr.test_all_indexers': 'medium',
'deluge.health': 'low',
'deluge.session_status': 'low',
'deluge.torrent_status': 'low',
'deluge.active_downloads': 'low',
'deluge.stalled_downloads': 'low',
'deluge.paused_downloads': 'low',
'deluge.speed_status': 'low',
'deluge.pause_torrent': 'medium',
'deluge.resume_torrent': 'medium',
'deluge.pause_all': 'high',
'deluge.resume_all': 'high',
'deluge.remove_torrent': 'critical',
'deluge.remove_torrent_data': 'critical',
'deluge.delete_data': 'critical',
'deluge.force_recheck': 'high',
'deluge.move_storage': 'high',
'deluge.add_torrent': 'high',
'deluge.add_magnet': 'high',
'deluge.mass_edit': 'critical',
'deluge.set_options': 'critical',
'deluge.unknown_mutation': 'critical',
'plex.health': 'low',
'plex.server_status': 'low',
'plex.library_status': 'low',
'plex.active_sessions': 'low',
'plex.recent_media': 'low',
'plex.scan_library': 'medium',
'plex.scan_all_libraries': 'high',
'plex.stream_summary': 'low',
'tautulli.health': 'low',
'tautulli.status': 'low',
'tautulli.activity': 'low',
'tautulli.history': 'medium',
'tautulli.users': 'medium',
'tautulli.libraries': 'low',
'tautulli.recently_added': 'low',
'tautulli.watch_stats': 'medium',
'tautulli.stream_summary': 'low',
'tautulli.plex_info': 'low',
'npm.health': 'low',
'npm.proxy_hosts': 'medium',
'npm.certificates': 'low',
'npm.expiring_certificates': 'low',
'npm.access_lists': 'medium',
'npm.exposure_summary': 'medium',
'npm.host_reachability': 'low',
'npm.security_warnings': 'low',
'npm.create_proxy_host': 'critical',
'npm.update_proxy_host': 'critical',
'npm.delete_proxy_host': 'critical',
'npm.enable_proxy_host': 'high',
'npm.disable_proxy_host': 'high',
'npm.renew_certificate': 'high',
'npm.delete_certificate': 'critical',
'npm.modify_access_list': 'critical',
'npm.create_user': 'critical',
'npm.update_user': 'critical',
'npm.delete_user': 'critical',
'npm.mass_edit': 'critical',
'npm.unknown_mutation': 'critical',
'authentik.health': 'low',
'authentik.system_status': 'low',
'authentik.applications': 'medium',
'authentik.providers': 'medium',
'authentik.proxy_providers': 'medium',
'authentik.oauth2_providers': 'medium',
'authentik.outposts': 'medium',
'authentik.outpost_health': 'low',
'authentik.flows_summary': 'medium',
'authentik.policies_summary': 'medium',
'authentik.events': 'medium',
'authentik.auth_coverage': 'medium',
'authentik.security_warnings': 'low',
'authentik.create_application': 'critical',
'authentik.update_application': 'critical',
'authentik.delete_application': 'critical',
'authentik.create_provider': 'critical',
'authentik.update_provider': 'critical',
'authentik.delete_provider': 'critical',
'authentik.create_outpost': 'critical',
'authentik.update_outpost': 'critical',
'authentik.delete_outpost': 'critical',
'authentik.create_policy': 'critical',
'authentik.update_policy': 'critical',
'authentik.delete_policy': 'critical',
'authentik.create_user': 'critical',
'authentik.update_user': 'critical',
'authentik.delete_user': 'critical',
'authentik.create_group': 'critical',
'authentik.update_group': 'critical',
'authentik.delete_group': 'critical',
'authentik.rotate_token': 'critical',
'authentik.force_sync': 'high',
'authentik.mass_edit': 'critical',
'authentik.unknown_mutation': 'critical',
'tautulli.terminate_session': 'high',
'tautulli.delete_newsletter': 'critical',
'tautulli.delete_notification_log': 'critical',
'tautulli.delete_notifier': 'critical',
'tautulli.delete_recently_added': 'critical',
'tautulli.download_config': 'critical',
'tautulli.download_database': 'critical',
'tautulli.download_export': 'critical',
'tautulli.download_log': 'critical',
'tautulli.download_plex_log': 'critical',
'tautulli.notify': 'high',
'tautulli.user_management': 'critical',
'tautulli.mass_edit': 'critical',
'tautulli.unknown_mutation': 'critical',
'plex.delete_media': 'critical',
'plex.delete_library': 'critical',
'plex.empty_trash': 'critical',
'plex.clean_bundles': 'critical',
'plex.optimize_database': 'critical',
'plex.kill_session': 'high',
'plex.user_management': 'critical',
'plex.force_refresh_all_metadata': 'critical',
'plex.mass_edit': 'critical',
'plex.unknown_mutation': 'critical',
'widefrog.delete_job': 'critical',
'widefrog.delete_media': 'critical',
'widefrog.clear_library': 'critical',
'widefrog.clear_queue': 'critical',
'widefrog.reset_database': 'critical',
'widefrog.delete_downloads': 'critical',
'widefrog.prune': 'critical',
'playbook.run': 'medium',
}
READ_ONLY_VERBS = {
'activity',
'active_downloads',
'active_sessions',
'application_status',
'applications',
'auth_coverage',
'certificates',
'check',
'download_status',
'diagnose',
'events',
'expiring_certificates',
'exposure_summary',
'failed_jobs',
'flows_summary',
'health',
'health_summary',
'history',
'host_reachability',
'indexer_health',
'indexer_status',
'libraries',
'library_status',
'logs',
'oauth2_providers',
'outpost_health',
'outposts',
'paused_downloads',
'plex_info',
'preflight',
'policies_summary',
'providers',
'proxy_hosts',
'proxy_providers',
'queue_status',
'recent_media',
'recently_added',
'search_movie',
'search_series',
'security_warnings',
'server_status',
'session_status',
'speed_status',
'stalled_downloads',
'status',
'stream_summary',
'summary',
'sync_status',
'system_status',
'torrent_status',
'users',
'wanted_missing',
'watch_stats',
'vms',
}
DESTRUCTIVE_VERBS = {
'clear_cache',
'clear_library',
'clear_logs',
'clear_queue',
'clean_bundles',
'delete_application',
'delete_certificate',
'delete_data',
'delete_downloads',
'delete_files',
'delete_group',
'delete_indexer',
'delete_job',
'delete_library',
'delete_media',
'delete_movie',
'delete_newsletter',
'delete_notification_log',
'delete_notifier',
'delete_outpost',
'delete_policy',
'delete_provider',
'delete_proxy_host',
'delete_recently_added',
'delete_series',
'delete_user',
'download_config',
'download_database',
'download_export',
'download_log',
'download_plex_log',
'empty_trash',
'force_refresh_all_metadata',
'grab_release',
'mass_edit',
'modify_access_list',
'prune',
'push_release',
'remove_torrent',
'remove_torrent_data',
'reset_database',
'rotate_token',
'set_options',
'stop',
'unknown_mutation',
}
def action_category(action: str) -> str:
return action.split('.', 1)[0] if '.' in action else 'general'
def action_default_target(action: str) -> str:
if action == 'controller.artifact.promote':
return 'tower/deployment-controller'
if action == 'controller.managed-storage.initialize':
return 'tower/deployment-controller'
if action == 'hub.migration.prepare_permissions':
return 'tower/itworx-mcp-hub-permissions'
if action == 'runner.fleet.upgrade_v2':
return 'tower/runner-fleet-v2'
if action == 'runner.fleet.specialize':
return 'tower/runner-fleet-specialized'
if action == 'runner.statistics.configure':
return 'tower/projectbrain-runner-statistics'
if action == 'deployment.targets.sync':
return 'jens/*'
if action == 'deployment.target.deploy_workmesh':
return 'jens/workmesh'
if action == 'deployment.target.promote_workmesh':
return 'jens/workmesh'
if action == 'deployment.target.register_portfolio':
return 'jens/portfolio'
if action in {
'deployment.target.register_privatemuse',
'deployment.webhook.repair_privatemuse',
}:
return 'jens/privatemuse'
if action == 'deployment.target.reconcile_media_codec_dashboard':
return 'jens/media-codec-dashboard'
category = action_category(action)
if category == 'container':
return 'container'
if category == 'volume':
return 'volume'
if category == 'media_pipeline':
return 'media-pipeline'
return category
def action_label(action: str) -> str:
return action.replace('_', ' ').replace('.', ' / ').title()
def action_capability(action: str) -> str:
verb = action.split('.', 1)[1] if '.' in action else action
if action in {'unraid.vm.stop', 'unraid.vm.reboot', 'unraid.vm.force_stop', 'unraid.vm.reset'}:
return 'destructive'
if verb in DESTRUCTIVE_VERBS or ACTION_RISK.get(action) == 'critical':
return 'destructive'
if verb in READ_ONLY_VERBS:
return 'read_only'
return 'operator'
def action_blast_radius(action: str, capability: str) -> list[str]:
category = action_category(action)
if capability == 'read_only':
return [f'Read-only visibility for {category}; no state changes are performed.']
if category == 'volume':
return ['Storage volume data and dependent apps could be affected if execution were ever enabled.']
if category == 'container':
return ['Target container availability and dependent routes may be affected.']
if category == 'stack':
return ['Multiple containers in the stack may restart or change state.']
if category == 'gateway':
return ['AppOps runtime visibility, cached connector state or operator context may be affected if execution were ever enabled.']
if category in {'authentik', 'npm'}:
return ['Authentication, exposure, routing or access-control behavior may be affected.']
if category in {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}:
return ['Media pipeline state, queues or user-facing media availability may be affected.']
if category == 'unraid':
return ['Unraid host or array operations may be affected.']
return [f'{category} operational state may be affected.']
def action_safeguards(action: str, decision: str, capability: str) -> list[str]:
safeguards = ['Registry detail is read-only and cannot execute this action.']
if capability == 'destructive':
safeguards.append('Destructive or critical actions stay blocked unless an explicit future policy path is designed.')
if decision == 'blocked':
safeguards.append('AppOps Gateway V1 exposes this only as a blocked boundary; no executor is registered.')
safeguards.append('Current policy decision is blocked.')
elif decision == 'approval_required':
safeguards.append('Current policy decision requires operator approval before execution.')
else:
safeguards.append('Allowed policy preview does not bypass preflight, audit or runtime checks.')
if 'token' in action or 'secret' in action:
safeguards.append('Secret-like values are never returned by the registry.')
return safeguards
def action_operator_guidance(action: str, decision: str, capability: str) -> str:
if decision == 'blocked':
return 'AppOps Gateway V1 blocks this action; treat it as governance visibility only and use external/manual tooling until a separate safe execution design exists.'
if decision == 'approval_required':
return 'Use approved operational flows only after reviewing impact and maintenance context.'
if capability == 'read_only':
return 'Safe to inspect for operational awareness.'
return 'Use the existing preflight and audited action flow before any live operation.'