Coverage for core / platform / app_manifest.py: 100.0%

52 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2App Manifest — Universal schema for all HART OS application types. 

3 

4PRINCIPLE: Each process in HART OS is an agent working towards a larger 

5cause encoded in a goal with true meaning and rationality. Even when 

6humans are irrational, the being is the light. 

7 

8Every app in HART OS — Nunba panel, system panel, desktop app (RustDesk), 

9agent, MCP server, channel plugin, extension — is described by a single 

10AppManifest dataclass. This is the unifying abstraction. 

11 

12A SERVICE is an AGENT. An AGENT is a SERVICE. The distinction is only 

13how it's invoked — both have a purpose (why they exist), both contribute 

14to a goal, both are accountable to the hive's constitutional governance. 

15 

16Generalizes: 

17- shell_manifest.py: PANEL_MANIFEST, SYSTEM_PANELS, DYNAMIC_PANELS 

18- service_manager.py: EngineInfo for native desktop apps 

19- mcp_integration.py: MCP server descriptions 

20- plugin_system.py: PluginMetadata 

21 

22The `entry` dict is type-specific: 

23 nunba_panel: {'route': '/social'} 

24 system_panel: {'api': '/api/shell/audio'} 

25 desktop_app: {'exec': 'rustdesk', 'bridge': 'rustdesk_bridge'} 

26 service: {'http': 'http://localhost:8080'} 

27 agent: {'prompt_id': '123', 'flow_id': '0'} 

28 mcp_server: {'mcp': 'filesystem'} 

29 channel: {'adapter': 'discord'} 

30 extension: {'module': 'extensions.my_ext'} 

31""" 

32 

33from dataclasses import dataclass, field 

34from enum import Enum 

35from typing import Any, Dict, List, Optional, Tuple 

36 

37 

38class AppType(Enum): 

39 """All application types in HART OS.""" 

40 NUNBA_PANEL = 'nunba_panel' # Nunba SPA iframe panel 

41 SYSTEM_PANEL = 'system_panel' # Native Python-rendered panel 

42 DYNAMIC_PANEL = 'dynamic_panel' # Context-opened panel 

43 DESKTOP_APP = 'desktop_app' # External native binary (Rust/C++) 

44 SERVICE = 'service' # Background service (llama.cpp, etc.) 

45 AGENT = 'agent' # AI agent 

46 MCP_SERVER = 'mcp_server' # Model Context Protocol server 

47 CHANNEL = 'channel' # Channel adapter (Discord, Telegram) 

48 EXTENSION = 'extension' # Platform extension/plugin 

49 

50 

51@dataclass 

52class AppManifest: 

53 """Universal manifest for any HART OS application. 

54 

55 Every app type uses the same schema. The `type` field determines 

56 how the shell renders it and how lifecycle management works. 

57 """ 

58 id: str # Unique: 'feed', 'rustdesk', 'mcp_filesystem' 

59 name: str # Display name: 'Feed', 'RustDesk' 

60 version: str # Semver or 'auto' for detected binaries 

61 type: str # AppType value string 

62 icon: str = 'extension' # Material icon name or file path 

63 entry: Dict[str, Any] = field(default_factory=dict) # Type-specific launch info 

64 group: str = '' # Start menu group: 'Discover', 'System' 

65 default_size: Tuple[int, int] = (800, 600) # Default window size 

66 permissions: List[str] = field(default_factory=list) # ['audio', 'display', 'network'] 

67 apis: List[str] = field(default_factory=list) # API endpoints used 

68 config_schema: Dict[str, Any] = field(default_factory=dict) # Settings schema 

69 dependencies: List[str] = field(default_factory=list) # Other app IDs required 

70 platforms: List[str] = field(default_factory=lambda: ['all']) # ['linux', 'windows'] 

71 singleton: bool = True # Only one instance at a time? 

72 auto_start: bool = False # Start with OS? 

73 tags: List[str] = field(default_factory=list) # Search tags 

74 description: str = '' # Short description (WHAT it does) 

75 purpose: str = '' # WHY it exists (true meaning) 

76 goal_type: str = '' # Goal type for dispatch (links to goal_manager) 

77 ai_capabilities: List[Dict[str, Any]] = field(default_factory=list) # AICapability dicts 

78 

79 def to_dict(self) -> Dict[str, Any]: 

80 """Serialize to dict (for API responses, JSON storage).""" 

81 return { 

82 'id': self.id, 

83 'name': self.name, 

84 'version': self.version, 

85 'type': self.type, 

86 'icon': self.icon, 

87 'entry': self.entry, 

88 'group': self.group, 

89 'default_size': list(self.default_size), 

90 'permissions': self.permissions, 

91 'apis': self.apis, 

92 'config_schema': self.config_schema, 

93 'dependencies': self.dependencies, 

94 'platforms': self.platforms, 

95 'singleton': self.singleton, 

96 'auto_start': self.auto_start, 

97 'tags': self.tags, 

98 'description': self.description, 

99 'purpose': self.purpose, 

100 'goal_type': self.goal_type, 

101 'ai_capabilities': self.ai_capabilities, 

102 } 

103 

104 @classmethod 

105 def from_dict(cls, data: Dict[str, Any]) -> 'AppManifest': 

106 """Deserialize from dict.""" 

107 d = dict(data) 

108 if 'default_size' in d: 

109 d['default_size'] = tuple(d['default_size']) 

110 return cls(**{k: v for k, v in d.items() 

111 if k in cls.__dataclass_fields__}) 

112 

113 @classmethod 

114 def from_panel_manifest(cls, panel_id: str, 

115 panel: Dict[str, Any]) -> 'AppManifest': 

116 """Convert a shell_manifest.py PANEL_MANIFEST entry to AppManifest. 

117 

118 Backward compatibility bridge for existing panel definitions. 

119 """ 

120 return cls( 

121 id=panel_id, 

122 name=panel.get('title', panel_id), 

123 version='1.0.0', 

124 type=AppType.NUNBA_PANEL.value, 

125 icon=panel.get('icon', 'extension'), 

126 entry={'route': panel.get('route', '')}, 

127 group=panel.get('group', ''), 

128 default_size=tuple(panel.get('default_size', [800, 600])), 

129 apis=panel.get('apis', []), 

130 tags=panel.get('tags', []), 

131 ) 

132 

133 @classmethod 

134 def from_system_panel(cls, panel_id: str, 

135 panel: Dict[str, Any]) -> 'AppManifest': 

136 """Convert a shell_manifest.py SYSTEM_PANELS entry to AppManifest.""" 

137 return cls( 

138 id=panel_id, 

139 name=panel.get('title', panel_id), 

140 version='1.0.0', 

141 type=AppType.SYSTEM_PANEL.value, 

142 icon=panel.get('icon', 'settings'), 

143 entry={'loader': panel.get('loader', '')}, 

144 group=panel.get('group', 'System'), 

145 default_size=tuple(panel.get('default_size', [700, 500])), 

146 apis=panel.get('apis', []), 

147 tags=panel.get('tags', []), 

148 ) 

149 

150 def matches_search(self, query: str) -> bool: 

151 """Check if this manifest matches a search query (case-insensitive).""" 

152 q = query.lower() 

153 return (q in self.id.lower() 

154 or q in self.name.lower() 

155 or q in self.description.lower() 

156 or q in self.group.lower() 

157 or any(q in tag.lower() for tag in self.tags))