pal-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. pal/__init__.py +29 -0
  2. pal/__main__.py +3 -0
  3. pal/access_manager/__init__.py +16 -0
  4. pal/access_manager/components/__init__.py +14 -0
  5. pal/access_manager/components/access_control.py +41 -0
  6. pal/access_manager/components/base.py +39 -0
  7. pal/access_manager/components/intervention.py +31 -0
  8. pal/access_manager/components/privilege_registry.py +30 -0
  9. pal/access_manager/pipeline.py +68 -0
  10. pal/access_manager/utils/__init__.py +2 -0
  11. pal/agent_constraints/__init__.py +3 -0
  12. pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
  13. pal/agent_constraints/components/__init__.py +3 -0
  14. pal/agent_constraints/components/constraints.py +22 -0
  15. pal/agent_constraints/pipeline.py +22 -0
  16. pal/agent_constraints/utils/__init__.py +2 -0
  17. pal/agents/__init__.py +12 -0
  18. pal/agents/coding.py +14 -0
  19. pal/agents/docs.py +15 -0
  20. pal/chat/__init__.py +4 -0
  21. pal/chat/demo.py +231 -0
  22. pal/chat/providers.py +23 -0
  23. pal/chat/service.py +591 -0
  24. pal/chat/services/__init__.py +26 -0
  25. pal/chat/services/base.py +142 -0
  26. pal/chat/services/data.py +142 -0
  27. pal/chat/services/media.py +123 -0
  28. pal/chat/services/web.py +60 -0
  29. pal/chat/sessions.py +108 -0
  30. pal/chat/tools.py +446 -0
  31. pal/chat/uploads.py +81 -0
  32. pal/cli.py +502 -0
  33. pal/compression/__init__.py +12 -0
  34. pal/compression/components/__init__.py +64 -0
  35. pal/compression/components/adverbs.py +20 -0
  36. pal/compression/components/hedges.py +54 -0
  37. pal/compression/components/interjections.py +20 -0
  38. pal/compression/components/minifier.py +56 -0
  39. pal/compression/components/output_compressor.py +13 -0
  40. pal/compression/components/pleonasms.py +54 -0
  41. pal/compression/components/punctuations.py +36 -0
  42. pal/compression/components/stopwords.py +62 -0
  43. pal/compression/history.py +346 -0
  44. pal/compression/pipeline.py +58 -0
  45. pal/compression/utils/__init__.py +30 -0
  46. pal/compression/utils/negations.py +18 -0
  47. pal/compression/utils/nltk_setup.py +20 -0
  48. pal/compression/utils/pronouns.py +27 -0
  49. pal/compression/utils/proselint_setup.py +9 -0
  50. pal/compression/utils/spacy_setup.py +22 -0
  51. pal/config/default.yaml +163 -0
  52. pal/config.py +504 -0
  53. pal/context_manager/__init__.py +7 -0
  54. pal/context_manager/chat_context.py +117 -0
  55. pal/context_manager/components/__init__.py +5 -0
  56. pal/context_manager/components/assembler.py +75 -0
  57. pal/context_manager/components/context_cache.py +45 -0
  58. pal/context_manager/components/kv_bridge.py +16 -0
  59. pal/context_manager/components/pager.py +48 -0
  60. pal/context_manager/components/simple_context_manager.py +56 -0
  61. pal/context_manager/components/turns.py +88 -0
  62. pal/context_manager/pipeline.py +27 -0
  63. pal/context_manager/utils/__init__.py +18 -0
  64. pal/context_manager/utils/hashing.py +5 -0
  65. pal/context_manager/utils/relevance.py +59 -0
  66. pal/context_manager/utils/storage.py +37 -0
  67. pal/documents/__init__.py +4 -0
  68. pal/documents/components/__init__.py +0 -0
  69. pal/documents/components/chunker.py +79 -0
  70. pal/documents/components/index.py +155 -0
  71. pal/documents/utils/__init__.py +1 -0
  72. pal/errors.py +40 -0
  73. pal/input_converters/__init__.py +8 -0
  74. pal/input_converters/components/__init__.py +0 -0
  75. pal/input_converters/components/base.py +38 -0
  76. pal/input_converters/components/pandoc_converter.py +87 -0
  77. pal/input_converters/components/pdf_converter.py +46 -0
  78. pal/input_converters/components/pptx_converter.py +52 -0
  79. pal/input_converters/components/xlsx_converter.py +38 -0
  80. pal/input_converters/pipeline.py +72 -0
  81. pal/input_converters/utils/__init__.py +1 -0
  82. pal/input_converters/workspace.py +56 -0
  83. pal/input_savers/INPUT_RULES.md +2 -0
  84. pal/input_savers/__init__.py +5 -0
  85. pal/input_savers/components/__init__.py +0 -0
  86. pal/input_savers/components/base.py +40 -0
  87. pal/input_savers/components/methods.py +41 -0
  88. pal/input_savers/components/verifier.py +39 -0
  89. pal/input_savers/pipeline.py +154 -0
  90. pal/input_savers/utils/__init__.py +0 -0
  91. pal/input_savers/utils/guards.py +94 -0
  92. pal/knowledge_graph/__init__.py +10 -0
  93. pal/knowledge_graph/components/__init__.py +0 -0
  94. pal/knowledge_graph/components/base.py +50 -0
  95. pal/knowledge_graph/components/builder.py +54 -0
  96. pal/knowledge_graph/components/retriever.py +100 -0
  97. pal/knowledge_graph/components/workspace.py +116 -0
  98. pal/knowledge_graph/pipeline.py +215 -0
  99. pal/knowledge_graph/utils/__init__.py +1 -0
  100. pal/latency_tracker/__init__.py +3 -0
  101. pal/latency_tracker/pipeline.py +86 -0
  102. pal/llm_scheduler/__init__.py +11 -0
  103. pal/llm_scheduler/api.py +137 -0
  104. pal/llm_scheduler/code.py +28 -0
  105. pal/llm_scheduler/core.py +92 -0
  106. pal/llm_scheduler/local.py +215 -0
  107. pal/llm_scheduler/pipeline.py +42 -0
  108. pal/llm_scheduler/types.py +92 -0
  109. pal/llm_wrapper/__init__.py +34 -0
  110. pal/llm_wrapper/adapter.py +316 -0
  111. pal/llm_wrapper/components/__init__.py +23 -0
  112. pal/llm_wrapper/components/adapter_types.py +90 -0
  113. pal/llm_wrapper/components/base.py +195 -0
  114. pal/llm_wrapper/components/features.py +104 -0
  115. pal/llm_wrapper/components/litellm_wrapper.py +215 -0
  116. pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
  117. pal/llm_wrapper/modes.py +73 -0
  118. pal/llm_wrapper/pipeline.py +61 -0
  119. pal/llm_wrapper/utils/__init__.py +4 -0
  120. pal/llm_wrapper/utils/registry.py +49 -0
  121. pal/llm_wrapper/utils/token_counting.py +10 -0
  122. pal/local_llm/__init__.py +5 -0
  123. pal/local_llm/components/__init__.py +0 -0
  124. pal/local_llm/components/huggingface_local.py +217 -0
  125. pal/local_llm/components/local_llm.py +60 -0
  126. pal/logging_setup.py +18 -0
  127. pal/memory/__init__.py +7 -0
  128. pal/memory/components/__init__.py +6 -0
  129. pal/memory/components/lru_k.py +56 -0
  130. pal/memory/components/manager.py +98 -0
  131. pal/memory/components/note.py +54 -0
  132. pal/memory/components/storage_manager.py +60 -0
  133. pal/memory/pipeline.py +122 -0
  134. pal/memory/utils/__init__.py +3 -0
  135. pal/memory/utils/keyword_overlap.py +3 -0
  136. pal/memory_tracker/__init__.py +3 -0
  137. pal/memory_tracker/components/__init__.py +0 -0
  138. pal/memory_tracker/components/process.py +56 -0
  139. pal/memory_tracker/pipeline.py +44 -0
  140. pal/metrics/__init__.py +4 -0
  141. pal/metrics/components/__init__.py +4 -0
  142. pal/metrics/components/collector.py +140 -0
  143. pal/metrics/components/exporter.py +28 -0
  144. pal/metrics/pipeline.py +49 -0
  145. pal/metrics/utils/__init__.py +2 -0
  146. pal/notes_manager/__init__.py +5 -0
  147. pal/notes_manager/components/__init__.py +0 -0
  148. pal/notes_manager/components/privacy.py +14 -0
  149. pal/notes_manager/components/recall.py +31 -0
  150. pal/notes_manager/components/tidy.py +28 -0
  151. pal/notes_manager/pipeline.py +138 -0
  152. pal/orchestrator/__init__.py +26 -0
  153. pal/orchestrator/components/__init__.py +26 -0
  154. pal/orchestrator/components/agent.py +105 -0
  155. pal/orchestrator/components/cgroup_manager.py +209 -0
  156. pal/orchestrator/components/context_interrupt.py +220 -0
  157. pal/orchestrator/components/limits.py +9 -0
  158. pal/orchestrator/components/scheduler.py +69 -0
  159. pal/orchestrator/components/syscall_scheduler.py +200 -0
  160. pal/orchestrator/pipeline.py +228 -0
  161. pal/orchestrator/utils/__init__.py +4 -0
  162. pal/orchestrator/utils/device.py +164 -0
  163. pal/orchestrator/utils/protocol.py +32 -0
  164. pal/os_kernel/__init__.py +5 -0
  165. pal/os_kernel/filesystem.py +80 -0
  166. pal/os_kernel/network.py +146 -0
  167. pal/os_kernel/pipeline.py +175 -0
  168. pal/os_kernel/sysinfo.py +36 -0
  169. pal/paths.py +59 -0
  170. pal/pipeline.py +177 -0
  171. pal/py.typed +0 -0
  172. pal/redact.py +19 -0
  173. pal/sdk/__init__.py +33 -0
  174. pal/sdk/adapters.py +43 -0
  175. pal/sdk/agent.py +99 -0
  176. pal/sdk/components/__init__.py +14 -0
  177. pal/sdk/components/autogen_adapter.py +65 -0
  178. pal/sdk/components/autogen_client.py +98 -0
  179. pal/sdk/components/framework_adapter_base.py +69 -0
  180. pal/sdk/components/metagpt_adapter.py +46 -0
  181. pal/sdk/components/open_interpreter_adapter.py +47 -0
  182. pal/sdk/components/tool_catalog.py +44 -0
  183. pal/sdk/os_api.py +84 -0
  184. pal/sdk/patterns.py +129 -0
  185. pal/sdk/pipeline.py +240 -0
  186. pal/sdk/queries.py +68 -0
  187. pal/sdk/savers.py +88 -0
  188. pal/sdk/utils/__init__.py +2 -0
  189. pal/storage_manager/__init__.py +17 -0
  190. pal/storage_manager/components/__init__.py +19 -0
  191. pal/storage_manager/components/base.py +50 -0
  192. pal/storage_manager/components/local_file_backend.py +84 -0
  193. pal/storage_manager/components/storage_queue.py +91 -0
  194. pal/storage_manager/components/vector_store_backend.py +122 -0
  195. pal/storage_manager/pipeline.py +153 -0
  196. pal/storage_manager/utils/__init__.py +4 -0
  197. pal/storage_manager/utils/memory_adapter.py +60 -0
  198. pal/storage_manager/utils/sdk.py +93 -0
  199. pal/studio/__init__.py +0 -0
  200. pal/studio/builder.py +166 -0
  201. pal/studio/catalog.py +90 -0
  202. pal/studio/checks.py +129 -0
  203. pal/studio/clients.py +6 -0
  204. pal/studio/editor.py +137 -0
  205. pal/studio/files.py +6 -0
  206. pal/studio/models.py +6 -0
  207. pal/studio/plan.py +149 -0
  208. pal/studio/planner.py +205 -0
  209. pal/studio/policy.py +69 -0
  210. pal/studio/routes.py +254 -0
  211. pal/studio/runs.py +210 -0
  212. pal/task_router/__init__.py +11 -0
  213. pal/task_router/components/__init__.py +0 -0
  214. pal/task_router/components/answerer.py +37 -0
  215. pal/task_router/components/base.py +34 -0
  216. pal/task_router/components/classifier.py +40 -0
  217. pal/task_router/components/gates.py +23 -0
  218. pal/task_router/pipeline.py +61 -0
  219. pal/task_router/utils/__init__.py +1 -0
  220. pal/terminal/__init__.py +1 -0
  221. pal/terminal/backend.py +141 -0
  222. pal/terminal/bench.py +173 -0
  223. pal/terminal/cli.py +826 -0
  224. pal/terminal/clients.py +170 -0
  225. pal/terminal/commands.py +401 -0
  226. pal/terminal/decide.py +242 -0
  227. pal/terminal/files.py +55 -0
  228. pal/terminal/flow_cli.py +439 -0
  229. pal/terminal/hardware.py +199 -0
  230. pal/terminal/history.py +88 -0
  231. pal/terminal/keys.py +83 -0
  232. pal/terminal/local_models.py +261 -0
  233. pal/terminal/logo.py +117 -0
  234. pal/terminal/models.py +99 -0
  235. pal/terminal/policy.py +54 -0
  236. pal/terminal/project.py +413 -0
  237. pal/terminal/review.py +65 -0
  238. pal/terminal/security.py +239 -0
  239. pal/terminal/session.py +503 -0
  240. pal/terminal/setup.py +354 -0
  241. pal/terminal/syntax.py +98 -0
  242. pal/terminal/theme.py +37 -0
  243. pal/terminal/ui.py +647 -0
  244. pal/terminal/voice.py +137 -0
  245. pal/terminal/workflow.py +624 -0
  246. pal/token_tracker/__init__.py +3 -0
  247. pal/token_tracker/pipeline.py +54 -0
  248. pal/tokens.py +21 -0
  249. pal/tool_catalog.py +107 -0
  250. pal/tool_manager/__init__.py +17 -0
  251. pal/tool_manager/builtin.py +82 -0
  252. pal/tool_manager/components/__init__.py +19 -0
  253. pal/tool_manager/components/base.py +53 -0
  254. pal/tool_manager/components/conflict_resolver.py +27 -0
  255. pal/tool_manager/components/param_validator.py +41 -0
  256. pal/tool_manager/components/tool_call_queue.py +111 -0
  257. pal/tool_manager/components/tool_registry.py +60 -0
  258. pal/tool_manager/pipeline.py +80 -0
  259. pal/tool_manager/utils/__init__.py +3 -0
  260. pal/web/__init__.py +3 -0
  261. pal/web/app.py +276 -0
  262. pal/web/static/editor.js +219 -0
  263. pal/web/static/fonts/LICENSE-Inter.txt +3 -0
  264. pal/web/static/fonts/inter-400.woff2 +0 -0
  265. pal/web/static/fonts/inter-500.woff2 +0 -0
  266. pal/web/static/fonts/inter-600.woff2 +0 -0
  267. pal/web/static/fonts/inter-700.woff2 +0 -0
  268. pal/web/static/inter.css +5 -0
  269. pal/web/static/pal-logo-256.png +0 -0
  270. pal/web/static/pal-logo-32.png +0 -0
  271. pal/web/static/pal-logo-64.png +0 -0
  272. pal/web/static/studio.css +306 -0
  273. pal/web/static/studio.js +533 -0
  274. pal/web/static/voice.js +76 -0
  275. pal/web/templates/index.html +368 -0
  276. pal/web/templates/studio.html +120 -0
  277. pal_code-0.1.0.dist-info/METADATA +379 -0
  278. pal_code-0.1.0.dist-info/RECORD +282 -0
  279. pal_code-0.1.0.dist-info/WHEEL +5 -0
  280. pal_code-0.1.0.dist-info/entry_points.txt +2 -0
  281. pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. pal_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,228 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from pal.access_manager.components.base import AccessDecision, OperationType
4
+ from pal.access_manager.pipeline import AccessManagerPipeline
5
+ from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
6
+ from pal.orchestrator.components.agent import AgentConfig
7
+ from pal.orchestrator.components.cgroup_manager import CgroupManager, create_resource_manager
8
+ from pal.orchestrator.components.scheduler import Scheduler
9
+ from pal.orchestrator.components.syscall_scheduler import SchedulingStrategy, SyscallRequest, SyscallScheduler
10
+ from pal.orchestrator.utils.device import ToolDevice
11
+ from pal.storage_manager.components.base import StorageSyscall
12
+ from pal.storage_manager.components.storage_queue import StorageRequest
13
+ from pal.storage_manager.pipeline import StorageManagerPipeline
14
+ from pal.storage_manager.utils.sdk import StorageSDK
15
+ from pal.tool_manager.components.base import ToolCallResult, ToolParam
16
+ from pal.tool_manager.components.tool_call_queue import QueuedToolCall
17
+ from pal.tool_manager.pipeline import ToolManagerPipeline
18
+
19
+ if TYPE_CHECKING:
20
+ from pal.memory.pipeline import MemoryPipeline
21
+
22
+
23
+ class OrchestratorPipeline:
24
+ """
25
+ Top-level entry point for agent orchestration: submit agent configs,
26
+ admit them onto a bounded number of concurrent cgroup-isolated
27
+ processes, and optionally expose a tool device those agent
28
+ processes can call back into.
29
+
30
+ Also owns the centralized syscall queue (SyscallScheduler): every
31
+ LLM inference request in the system, from any module, is meant to
32
+ go through submit_syscall() here rather than each caller hitting an
33
+ LLMWrapper directly -- that's what "centralize all queues within the
34
+ scheduler module" means in practice (AIOS 3.3).
35
+
36
+ And owns the Storage Manager (AIOS 3.6): one StorageManagerPipeline
37
+ shared by every agent/pipeline in the system, for both the memory
38
+ manager's RAM-to-disk swaps (see pal/memory/pipeline.py's
39
+ storage_manager= parameter) and an agent's own direct storage SDK
40
+ calls (storage_sdk_for()).
41
+
42
+ And owns the Tool Manager (AIOS 3.7): one ToolManagerPipeline
43
+ (standardized loading, parameter validation, parallel-access
44
+ conflict resolution), wired into the ToolDevice transport so every
45
+ tool call an agent makes over the socket gets the same treatment
46
+ regardless of which agent or which tool.
47
+
48
+ And owns the Access Manager (AIOS 3.8): one AccessManagerPipeline,
49
+ wired into the StorageManagerPipeline so every cross-agent storage
50
+ request (StorageSDK.read_from()/write_to()/delete_from()) is
51
+ checked against a privilege-group hashmap, and every destructive
52
+ operation (delete, overwrite) goes through user intervention.
53
+ Agents are assigned a privilege group via assign_privilege_group()
54
+ or submit_agent(..., privilege_group=...).
55
+
56
+ And provides the AIOS SDK (AIOS 3.9) via sdk_for(agent_id,
57
+ core_config): one AgentSDK per agent, wired to every manager above,
58
+ which is the actual interface an agent developer builds against --
59
+ chat()/remember()/recall()/read()/write()/call_tool()/etc. -- rather
60
+ than any manager or syscall type directly.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ parent_slice: str = "pal.slice",
66
+ max_concurrent: int = 3,
67
+ tool_socket_path: str = None, # a Unix socket path, or host:port (used on Windows)
68
+ syscall_strategy: SchedulingStrategy = SchedulingStrategy.FIFO,
69
+ rr_quantum: float = 2.0,
70
+ storage_root: str = None,
71
+ vector_persist_directory: str = None,
72
+ confirmation_handler=None,
73
+ resource_manager=None,
74
+ ):
75
+ self.cgroup_manager = resource_manager or create_resource_manager(parent_slice, manager_cls=CgroupManager)
76
+ self.scheduler = Scheduler(self.cgroup_manager, max_concurrent=max_concurrent)
77
+ self.syscall_scheduler = SyscallScheduler(strategy=syscall_strategy, rr_quantum=rr_quantum)
78
+ self.access_manager = AccessManagerPipeline(confirmation_handler=confirmation_handler)
79
+ self.storage_manager = StorageManagerPipeline(
80
+ root=storage_root, vector_persist_directory=vector_persist_directory, access_manager=self.access_manager
81
+ )
82
+ self.tool_manager = ToolManagerPipeline()
83
+ self.tool_device = ToolDevice(tool_socket_path, tool_manager=self.tool_manager) if tool_socket_path \
84
+ else ToolDevice(tool_manager=self.tool_manager)
85
+
86
+ def submit_agent(self, config: AgentConfig, priority: int = 0, weight: int = 100, privilege_group: str = None) -> None:
87
+ self.scheduler.submit(config, priority=priority, weight=weight)
88
+ if privilege_group is not None:
89
+ self.access_manager.assign_privilege_group(config.name, privilege_group)
90
+
91
+ def tick(self) -> list[str]:
92
+ return self.scheduler.tick()
93
+
94
+ def run_turn(self, agent_name: str, system_prompt: str, user_message: str, timeout: float = 60) -> dict:
95
+ agent = self.scheduler.running.get(agent_name)
96
+ if agent is None:
97
+ raise ValueError(f"agent {agent_name} is not running -- submit + tick first")
98
+ return agent.run_turn(system_prompt, user_message, timeout=timeout)
99
+
100
+ # -- centralized syscall scheduling (AIOS 3.3) -----------------------------
101
+
102
+ def submit_syscall(self, agent_name: str, core: LLMWrapper, syscall: LLMSyscall, priority: int = 0) -> str:
103
+ """Every LLM inference request funnels through here rather than
104
+ callers hitting core.chat()/core.stream_chat() directly, so
105
+ request ordering and time-slicing are handled in exactly one
106
+ place regardless of which module -- an Agent, the top-level
107
+ Pipeline, anything else -- issued the request."""
108
+ return self.syscall_scheduler.submit(agent_name, core, syscall, priority=priority)
109
+
110
+ def syscall_tick(self) -> SyscallRequest | None:
111
+ """Advances the syscall queue by one step, using whichever
112
+ strategy this orchestrator was configured with (FIFO or Round
113
+ Robin)."""
114
+ return self.syscall_scheduler.tick()
115
+
116
+ def drain_syscalls(self, max_ticks: int = 10_000) -> list[SyscallRequest]:
117
+ return self.syscall_scheduler.drain(max_ticks=max_ticks)
118
+
119
+ def syscall_result(self, request_id: str) -> SyscallRequest:
120
+ return self.syscall_scheduler.get_result(request_id)
121
+
122
+ # -- Storage Manager (AIOS 3.6) --------------------------------------------
123
+
124
+ def storage_sdk_for(self, agent_id: str) -> StorageSDK:
125
+ """What an agent calls to read/write its own persistent data --
126
+ "the agent calls the storage API in the SDK, which is further
127
+ converted into storage-related system calls and put into the
128
+ storage queue." Every StorageSDK instance shares this
129
+ orchestrator's one StorageManagerPipeline/queue, scoped by
130
+ agent_id."""
131
+ return StorageSDK(agent_id, self.storage_manager)
132
+
133
+ def submit_storage_syscall(self, syscall: StorageSyscall) -> str:
134
+ return self.storage_manager.submit(syscall)
135
+
136
+ def storage_tick(self) -> StorageRequest | None:
137
+ return self.storage_manager.tick()
138
+
139
+ def drain_storage_syscalls(self, max_iterations: int = 10_000) -> list[StorageRequest]:
140
+ return self.storage_manager.drain(max_iterations=max_iterations)
141
+
142
+ def storage_result(self, request_id: str) -> StorageRequest:
143
+ return self.storage_manager.get_result(request_id)
144
+
145
+ # -- Access Manager (AIOS 3.8) ----------------------------------------------
146
+
147
+ def assign_privilege_group(self, agent_id: str, group: str) -> None:
148
+ self.access_manager.assign_privilege_group(agent_id, group)
149
+
150
+ def set_confirmation_handler(self, handler) -> None:
151
+ """Registers the callback that answers user-intervention
152
+ prompts for irreversible operations (delete, overwrite,
153
+ privilege modification). The default handler denies everything,
154
+ so a real application needs to set one -- e.g. a CLI y/n prompt
155
+ or a UI confirmation dialog."""
156
+ self.access_manager.set_confirmation_handler(handler)
157
+
158
+ def check_access(self, requesting_agent_id: str, target_agent_id: str, operation: OperationType, resource: str = "") -> AccessDecision:
159
+ return self.access_manager.check_access(requesting_agent_id, target_agent_id, operation, resource=resource)
160
+
161
+ def register_tool(self, name: str, loader, params: list[ToolParam] = None,
162
+ parallel_limit: int = None, dependencies: list[str] = None) -> None:
163
+ """Registers a tool with the Tool Manager (AIOS 3.7): `loader`
164
+ is called at most once, the first time this tool is actually
165
+ invoked (dynamic loading -- see ToolRegistry.get_callable).
166
+ Reachable both via the ToolDevice socket transport and directly
167
+ through invoke_tool() below."""
168
+ self.tool_device.register_tool(
169
+ name, loader, params=params, parallel_limit=parallel_limit, dependencies=dependencies
170
+ )
171
+
172
+ def invoke_tool(self, agent_id: str, tool_name: str, args: dict) -> ToolCallResult:
173
+ return self.tool_manager.invoke(agent_id, tool_name, args)
174
+
175
+ def submit_tool_call(self, agent_id: str, tool_name: str, args: dict) -> str:
176
+ return self.tool_manager.submit(agent_id, tool_name, args)
177
+
178
+ def dispatch_tool_call(self) -> QueuedToolCall | None:
179
+ return self.tool_manager.dispatch_next()
180
+
181
+ def drain_tool_calls(self, max_iterations: int = 10_000) -> list[QueuedToolCall]:
182
+ return self.tool_manager.drain(max_iterations=max_iterations)
183
+
184
+ # -- AIOS SDK (AIOS 3.9) -----------------------------------------------------
185
+
186
+ def sdk_for(self, agent_id: str, core_config, memory: Optional["MemoryPipeline"] = None):
187
+ """Builds an AgentSDK for one agent, wired to this
188
+ orchestrator's shared managers (storage, tools, access control)
189
+ and centralized LLM syscall scheduler -- the single object an
190
+ agent developer actually interacts with, abstracting away every
191
+ manager and syscall type above it. `core_config` is
192
+ agent-specific (different agents may run against different
193
+ backends/models), everything else is shared."""
194
+ from pal.llm_wrapper.pipeline import LLMWrapperPipeline
195
+ from pal.sdk.pipeline import AgentSDK
196
+
197
+ llm_wrapper = LLMWrapperPipeline(core_config)
198
+ return AgentSDK(
199
+ agent_id=agent_id,
200
+ llm_wrapper=llm_wrapper,
201
+ storage_manager=self.storage_manager,
202
+ tool_manager=self.tool_manager,
203
+ access_manager=self.access_manager,
204
+ memory=memory,
205
+ syscall_scheduler=self.syscall_scheduler,
206
+ )
207
+
208
+ def register_builtin_tools(self) -> None:
209
+ """Opt-in: registers the SDK's small built-in tool catalog
210
+ (pal/sdk/components/tool_catalog.py) with this
211
+ orchestrator's ToolManagerPipeline. Not called automatically --
212
+ a fresh OrchestratorPipeline starts with no tools registered,
213
+ exactly as before this existed; call this explicitly to make
214
+ the built-in catalog available."""
215
+ from pal.sdk.components.tool_catalog import register_builtin_tools
216
+ register_builtin_tools(self.tool_manager)
217
+
218
+ def start_tool_device(self) -> None:
219
+ self.tool_device.start()
220
+
221
+ def stop_tool_device(self) -> None:
222
+ self.tool_device.stop()
223
+
224
+ def status(self) -> dict:
225
+ return self.scheduler.status()
226
+
227
+ def stop_all(self) -> None:
228
+ self.scheduler.stop_all()
@@ -0,0 +1,4 @@
1
+ from pal.orchestrator.utils.device import DEFAULT_SOCKET_PATH, ToolDevice, call_tool
2
+ from pal.orchestrator.utils.protocol import ToolRequest, ToolResponse
3
+
4
+ __all__ = ["ToolDevice", "call_tool", "DEFAULT_SOCKET_PATH", "ToolRequest", "ToolResponse"]
@@ -0,0 +1,164 @@
1
+ """Tool transport between agent processes and the orchestrator.
2
+
3
+ Portable: a Unix domain socket where the OS has one (Linux, macOS), a loopback TCP socket where it
4
+ does not (Windows). Requests and responses are one JSON object per line, so a message of any size
5
+ is read completely (the old single recv() truncated anything over 64 KB). The Unix socket file is
6
+ created owner-only (0600); the TCP fallback binds 127.0.0.1 only.
7
+
8
+ Stands in for a future /dev/pal-tools character device: porting later means swapping this
9
+ transport, not the protocol or how tool calls are handled.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ import re
16
+ import socket
17
+ import tempfile
18
+ import threading
19
+ from collections.abc import Callable
20
+ from typing import Optional
21
+
22
+ from pal.orchestrator.utils.protocol import ToolRequest, ToolResponse
23
+ from pal.tool_manager.pipeline import ToolManagerPipeline
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+ HAS_UNIX_SOCKETS = hasattr(socket, "AF_UNIX")
28
+ DEFAULT_SOCKET_PATH = (
29
+ os.path.join(tempfile.gettempdir(), "pal-tools.sock") if HAS_UNIX_SOCKETS else "127.0.0.1:47613")
30
+ MAX_MESSAGE_BYTES = 1_000_000
31
+ _TCP = re.compile(r"^[A-Za-z0-9.\-]+:\d+$")
32
+
33
+
34
+ def _is_tcp(address: str) -> bool:
35
+ return bool(_TCP.match(address))
36
+
37
+
38
+ def _recv_line(conn: socket.socket) -> bytes:
39
+ buf = bytearray()
40
+ while b"\n" not in buf:
41
+ chunk = conn.recv(65536)
42
+ if not chunk:
43
+ break
44
+ buf += chunk
45
+ if len(buf) > MAX_MESSAGE_BYTES:
46
+ raise ValueError("message too large")
47
+ return bytes(buf)
48
+
49
+
50
+ def _connect(address: str, timeout: float) -> socket.socket:
51
+ if _is_tcp(address) or not HAS_UNIX_SOCKETS:
52
+ host, _, port = address.rpartition(":")
53
+ return socket.create_connection((host or "127.0.0.1", int(port)), timeout=timeout)
54
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
55
+ sock.settimeout(timeout)
56
+ sock.connect(address)
57
+ return sock
58
+
59
+
60
+ class ToolDevice:
61
+ """Agents (in their own OS process) connect here to invoke tools; the orchestrator's process
62
+ executes them, giving one place to apply permissions and auditing per tool call.
63
+
64
+ With a ToolManagerPipeline, register_tool() registers a full ToolSpec (loader, params,
65
+ parallel_limit, dependencies). Without one it falls back to a plain name -> callable dict.
66
+ `address` is a Unix socket path or "host:port"; after start(), `.address` is the real one
67
+ (port 0 picks a free port)."""
68
+
69
+ def __init__(self, address: Optional[str] = None, tool_manager: Optional[ToolManagerPipeline] = None,
70
+ socket_path: Optional[str] = None):
71
+ self.address = address or socket_path or DEFAULT_SOCKET_PATH
72
+ self.tool_manager = tool_manager
73
+ self._handlers: dict[str, Callable[[dict], dict]] = {}
74
+ self._server: Optional[socket.socket] = None
75
+ self._thread: Optional[threading.Thread] = None
76
+ self._running = False
77
+
78
+ @property
79
+ def socket_path(self) -> str: # backwards-compatible name
80
+ return self.address
81
+
82
+ def register_tool(self, name: str, handler: Callable[[dict], dict], **tool_spec_kwargs) -> None:
83
+ if self.tool_manager is not None:
84
+ self.tool_manager.register_tool(name, handler, **tool_spec_kwargs)
85
+ else:
86
+ self._handlers[name] = handler
87
+
88
+ def start(self) -> None:
89
+ if self._running:
90
+ return
91
+ if _is_tcp(self.address) or not HAS_UNIX_SOCKETS:
92
+ host, _, port = self.address.rpartition(":")
93
+ self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
94
+ self._server.bind((host or "127.0.0.1", int(port or 0)))
95
+ self.address = f"{self._server.getsockname()[0]}:{self._server.getsockname()[1]}"
96
+ else:
97
+ if os.path.exists(self.address):
98
+ os.remove(self.address)
99
+ self._server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
100
+ self._server.bind(self.address)
101
+ os.chmod(self.address, 0o600) # owner only
102
+ self._server.listen(5)
103
+ self._running = True
104
+ self._thread = threading.Thread(target=self._serve_loop, daemon=True)
105
+ self._thread.start()
106
+
107
+ def _serve_loop(self) -> None:
108
+ while self._running:
109
+ try:
110
+ self._server.settimeout(0.5)
111
+ conn, _ = self._server.accept()
112
+ except TimeoutError:
113
+ continue
114
+ except OSError:
115
+ break
116
+ threading.Thread(target=self._handle_conn, args=(conn,), daemon=True).start()
117
+
118
+ def _handle_conn(self, conn: socket.socket) -> None:
119
+ with conn:
120
+ conn.settimeout(30)
121
+ try:
122
+ raw = _recv_line(conn)
123
+ if not raw:
124
+ return
125
+ req = ToolRequest.decode(raw)
126
+ if self.tool_manager is not None:
127
+ result = self.tool_manager.invoke(req.agent_name, req.tool_name, req.args)
128
+ resp = ToolResponse(ok=result.ok, result=result.result, error=result.error)
129
+ else:
130
+ handler = self._handlers.get(req.tool_name)
131
+ if handler is None:
132
+ resp = ToolResponse(ok=False, error=f"unknown tool: {req.tool_name}")
133
+ else:
134
+ resp = ToolResponse(ok=True, result=handler(req.args))
135
+ except Exception as e: # noqa: BLE001 (any failure becomes an error response)
136
+ log.warning("tool request failed: %s", e)
137
+ resp = ToolResponse(ok=False, error=str(e))
138
+ try:
139
+ conn.sendall(resp.encode())
140
+ except OSError:
141
+ pass
142
+
143
+ def stop(self) -> None:
144
+ self._running = False
145
+ if self._server:
146
+ self._server.close()
147
+ if HAS_UNIX_SOCKETS and not _is_tcp(self.address) and os.path.exists(self.address):
148
+ os.remove(self.address)
149
+
150
+
151
+ def call_tool(agent_name: str, tool_name: str, args: dict, socket_path: Optional[str] = None,
152
+ timeout: float = 30, address: Optional[str] = None) -> dict:
153
+ """Client-side helper: what an agent process calls to invoke a tool."""
154
+ req = ToolRequest(agent_name=agent_name, tool_name=tool_name, args=args)
155
+ sock = _connect(address or socket_path or DEFAULT_SOCKET_PATH, timeout)
156
+ try:
157
+ sock.sendall(req.encode())
158
+ raw = _recv_line(sock)
159
+ finally:
160
+ sock.close()
161
+ resp = ToolResponse.decode(raw)
162
+ if not resp.ok:
163
+ raise RuntimeError(f"tool call failed: {resp.error}")
164
+ return resp.result
@@ -0,0 +1,32 @@
1
+ import json
2
+ from dataclasses import asdict, dataclass
3
+
4
+
5
+ @dataclass
6
+ class ToolRequest:
7
+ agent_name: str
8
+ tool_name: str
9
+ args: dict
10
+
11
+ def encode(self) -> bytes:
12
+ return (json.dumps(asdict(self)) + "\n").encode("utf-8")
13
+
14
+ @staticmethod
15
+ def decode(raw: bytes) -> "ToolRequest":
16
+ data = json.loads(raw.decode("utf-8"))
17
+ return ToolRequest(**data)
18
+
19
+
20
+ @dataclass
21
+ class ToolResponse:
22
+ ok: bool
23
+ result: object = None
24
+ error: str | None = None
25
+
26
+ def encode(self) -> bytes:
27
+ return (json.dumps(asdict(self)) + "\n").encode("utf-8")
28
+
29
+ @staticmethod
30
+ def decode(raw: bytes) -> "ToolResponse":
31
+ data = json.loads(raw.decode("utf-8"))
32
+ return ToolResponse(**data)
@@ -0,0 +1,5 @@
1
+ from pal.os_kernel.filesystem import OSKernelError, Workspace
2
+ from pal.os_kernel.pipeline import OS_OPS, OSKernel
3
+ from pal.os_kernel.sysinfo import system_info
4
+
5
+ __all__ = ["OS_OPS", "OSKernel", "OSKernelError", "Workspace", "system_info"]
@@ -0,0 +1,80 @@
1
+ """File operations: the part of the OS kernel's filesystem an agent may touch.
2
+
3
+ Every agent gets its own workspace folder. A path must be relative and stay inside it: no `..`, absolute paths,
4
+ drive letters, backslashes or names Windows forbids, and a symlink that points outside is refused. Writes are atomic.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from pal.errors import UserInputError
11
+ from pal.paths import atomic_write_bytes, is_safe_relative_key
12
+
13
+
14
+ class OSKernelError(UserInputError):
15
+ """A request to the OS kernel was refused or failed. The message is safe to show."""
16
+
17
+
18
+ class Workspace:
19
+ def __init__(self, root: Path, max_read: int, max_write: int, max_binary: int = 20_000_000):
20
+ self.root = Path(root)
21
+ self.root.mkdir(parents=True, exist_ok=True)
22
+ self.max_read = max_read
23
+ self.max_write = max_write
24
+ self.max_binary = max_binary
25
+
26
+ def resolve(self, rel: str) -> Path:
27
+ if not isinstance(rel, str) or not is_safe_relative_key(rel):
28
+ raise OSKernelError(f"invalid path: {rel!r} (use a relative path inside your workspace)")
29
+ target = (self.root / rel).resolve()
30
+ if self.root.resolve() not in target.parents:
31
+ raise OSKernelError(f"path escapes the workspace: {rel!r}")
32
+ return target
33
+
34
+ def exists(self, rel: str) -> bool:
35
+ return self.resolve(rel).is_file()
36
+
37
+ def read(self, rel: str) -> str:
38
+ p = self.resolve(rel)
39
+ if not p.is_file():
40
+ raise OSKernelError(f"no such file: {rel}")
41
+ if p.stat().st_size > self.max_read:
42
+ raise OSKernelError(f"{rel} is larger than the {self.max_read:,}-byte read limit")
43
+ return p.read_text(encoding="utf-8", errors="replace")
44
+
45
+ def read_bytes(self, rel: str) -> bytes:
46
+ p = self.resolve(rel)
47
+ if not p.is_file():
48
+ raise OSKernelError(f"no such file: {rel}")
49
+ if p.stat().st_size > self.max_binary:
50
+ raise OSKernelError(f"{rel} is larger than the {self.max_binary:,}-byte limit for binary files")
51
+ return p.read_bytes()
52
+
53
+ def write_bytes(self, rel: str, data: bytes) -> int:
54
+ if len(data) > self.max_binary:
55
+ raise OSKernelError(f"the data is larger than the {self.max_binary:,}-byte limit for binary files")
56
+ atomic_write_bytes(self.resolve(rel), bytes(data))
57
+ return len(data)
58
+
59
+ def write(self, rel: str, content: str) -> int:
60
+ data = str(content).encode("utf-8")
61
+ if len(data) > self.max_write:
62
+ raise OSKernelError(f"content is larger than the {self.max_write:,}-byte write limit")
63
+ atomic_write_bytes(self.resolve(rel), data)
64
+ return len(data)
65
+
66
+ def delete(self, rel: str) -> None:
67
+ p = self.resolve(rel)
68
+ if not p.is_file():
69
+ raise OSKernelError(f"no such file: {rel}")
70
+ p.unlink()
71
+
72
+ def list(self, prefix: str = "") -> list[str]:
73
+ base = self.root.resolve()
74
+ out = []
75
+ for p in sorted(base.rglob("*")):
76
+ if p.is_file() and not p.name.endswith(".tmp"):
77
+ rel = p.relative_to(base).as_posix()
78
+ if rel.startswith(prefix):
79
+ out.append(rel)
80
+ return out
@@ -0,0 +1,146 @@
1
+ """Network requests through the OS network stack, with the checks an agent-facing call needs.
2
+
3
+ http and https only; no credentials in the URL; the host must resolve to a public address unless the settings allow
4
+ private ones (blocks requests to localhost, the home network and cloud metadata endpoints); an optional host
5
+ allow-list; a timeout; a response size cap; redirects are followed by hand so every hop is checked again.
6
+ Residual risk, stated plainly: the address is checked before connecting, so a hostile DNS server that answers
7
+ differently the second time (DNS rebinding) is not fully covered.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import ipaddress
13
+ import logging
14
+ import socket
15
+ import threading
16
+ from collections.abc import Iterator
17
+ from typing import Any
18
+ from urllib.parse import urljoin, urlsplit
19
+
20
+ from pal.os_kernel.filesystem import OSKernelError
21
+
22
+ _MAX_REDIRECTS = 3
23
+
24
+
25
+ def check_url(url: str, allow_private: bool, allowed_hosts: list[str]) -> None:
26
+ p = urlsplit(url)
27
+ if p.scheme not in ("http", "https") or not p.hostname:
28
+ raise OSKernelError("only http and https URLs are allowed")
29
+ if p.username or p.password:
30
+ raise OSKernelError("credentials inside a URL are not allowed")
31
+ host = p.hostname.lower()
32
+ if allowed_hosts and host not in {h.lower() for h in allowed_hosts}:
33
+ raise OSKernelError(f"host {host!r} is not on the allowed list")
34
+ try:
35
+ infos = socket.getaddrinfo(host, p.port or (443 if p.scheme == "https" else 80), type=socket.SOCK_STREAM)
36
+ except OSError as e:
37
+ raise OSKernelError(f"could not resolve {host!r}") from e
38
+ if allow_private:
39
+ return
40
+ for info in infos:
41
+ ip = ipaddress.ip_address(info[4][0])
42
+ if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified:
43
+ raise OSKernelError(f"{host!r} resolves to a private or reserved address, which is blocked")
44
+
45
+
46
+ # Headers a caller may set. Everything else (Host, Cookie, Proxy-*, X-Forwarded-*, ...) is refused, so a request cannot be made
47
+ # to look like it came from somewhere else. These are the ones service APIs use for their keys.
48
+ SAFE_HEADERS = frozenset({"authorization", "x-api-key", "x-rapidapi-key", "x-rapidapi-host", "ocp-apim-subscription-key",
49
+ "x-goog-api-key", "x-goog-fieldmask", "content-type", "accept"})
50
+ _TEXT_TYPES = ("application/json", "application/xml", "application/atom+xml", "application/xhtml+xml", "application/javascript")
51
+
52
+
53
+ def clean_headers(headers: Any) -> dict:
54
+ out: dict = {}
55
+ for k, v in (headers or {}).items():
56
+ if not isinstance(k, str) or not isinstance(v, str):
57
+ raise OSKernelError("header names and values must be text")
58
+ if k.lower() not in SAFE_HEADERS:
59
+ raise OSKernelError(f"header {k!r} is not allowed")
60
+ if len(v) > 2000 or any(c in v for c in "\r\n\x00"):
61
+ raise OSKernelError(f"header {k!r} has an invalid value")
62
+ out[k] = v
63
+ return out
64
+
65
+
66
+ def is_text(content_type: str) -> bool:
67
+ ct = content_type.split(";")[0].strip().lower()
68
+ return not ct or ct.startswith("text/") or ct in _TEXT_TYPES or ct.endswith(("+json", "+xml"))
69
+
70
+
71
+ def safe_target(url: str) -> str:
72
+ """The URL without its query string, fragment or credentials: what is written to logs, because the query may hold an API key."""
73
+ p = urlsplit(url)
74
+ if not (p.scheme and p.hostname):
75
+ return ""
76
+ return f"{p.scheme}://{p.hostname}{f':{p.port}' if p.port else ''}{p.path}"
77
+
78
+
79
+ _quiet_lock = threading.Lock()
80
+ _quiet_depth = 0
81
+ _quiet_saved = logging.NOTSET
82
+
83
+
84
+ @contextlib.contextmanager
85
+ def _quiet_http_logs() -> Iterator[None]:
86
+ """urllib3's DEBUG log line holds the full request URL, query string included, and so would put an API key in a log file for
87
+ anyone running with debug logging. Silence it for the length of a request (counted, so overlapping requests are safe)."""
88
+ global _quiet_depth, _quiet_saved
89
+ lg = logging.getLogger("urllib3")
90
+ with _quiet_lock:
91
+ if _quiet_depth == 0:
92
+ _quiet_saved = lg.level
93
+ lg.setLevel(logging.WARNING)
94
+ _quiet_depth += 1
95
+ try:
96
+ yield
97
+ finally:
98
+ with _quiet_lock:
99
+ _quiet_depth -= 1
100
+ if _quiet_depth == 0:
101
+ lg.setLevel(_quiet_saved)
102
+
103
+
104
+ def request(method: str, url: str, *, allow_private: bool, allowed_hosts: list[str], timeout: float, max_bytes: int,
105
+ json: Any = None, headers: dict | None = None, data: bytes | None = None, max_binary_bytes: int | None = None,
106
+ keep_binary: bool = False) -> dict:
107
+ """One HTTP request with the checks above. `headers` must already be cleaned. A text answer comes back as `text` (at most
108
+ `max_bytes`); with `keep_binary` any other answer comes back as `body` bytes (at most `max_binary_bytes`) and `text` is empty.
109
+ Redirects are followed by hand and the caller's headers are NOT sent to the new address (they hold keys)."""
110
+ with _quiet_http_logs():
111
+ return _request(method, url, allow_private, allowed_hosts, timeout, max_bytes, json, headers, data, max_binary_bytes, keep_binary)
112
+
113
+
114
+ def _request(method: str, url: str, allow_private: bool, allowed_hosts: list[str], timeout: float, max_bytes: int, json: Any,
115
+ headers: dict | None, data: bytes | None, max_binary_bytes: int | None, keep_binary: bool) -> dict:
116
+ import requests
117
+
118
+ sent = dict(headers or {})
119
+ for _ in range(_MAX_REDIRECTS + 1):
120
+ check_url(url, allow_private, allowed_hosts)
121
+ try:
122
+ resp = requests.request(method, url, json=json, data=data, timeout=timeout, allow_redirects=False, stream=True,
123
+ headers={"User-Agent": "pal-code", **sent})
124
+ except requests.RequestException as e:
125
+ raise OSKernelError(f"request failed: {type(e).__name__}") from e
126
+ if resp.is_redirect and resp.headers.get("Location"):
127
+ url, json, data, method, sent = urljoin(url, resp.headers["Location"]), None, None, "GET", {}
128
+ resp.close()
129
+ continue
130
+ ctype = resp.headers.get("Content-Type", "")
131
+ textual = is_text(ctype) or not keep_binary
132
+ cap = max_bytes if textual else (max_binary_bytes or max_bytes)
133
+ body = b""
134
+ for chunk in resp.iter_content(8192):
135
+ body += chunk
136
+ if len(body) > cap:
137
+ body = body[:cap]
138
+ break
139
+ resp.close()
140
+ out = {"status": resp.status_code, "content_type": ctype, "truncated": len(body) >= cap, "bytes": len(body)}
141
+ if textual:
142
+ out["text"] = body.decode(resp.encoding or "utf-8", errors="replace")
143
+ else:
144
+ out["text"], out["body"] = "", body
145
+ return out
146
+ raise OSKernelError("too many redirects")