rubik 0.1.0

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 (523) hide show
  1. checksums.yaml +7 -0
  2. data/ATTRIBUTION.md +139 -0
  3. data/CHANGELOG.md +236 -0
  4. data/LICENSE +21 -0
  5. data/README.md +269 -0
  6. data/bin/rubik +82 -0
  7. data/lib/rubik/adapter/base.rb +195 -0
  8. data/lib/rubik/adapter/c.rb +36 -0
  9. data/lib/rubik/adapter/crystal.rb +78 -0
  10. data/lib/rubik/adapter/generic.rb +86 -0
  11. data/lib/rubik/adapter/go.rb +36 -0
  12. data/lib/rubik/adapter/node.rb +146 -0
  13. data/lib/rubik/adapter/process.rb +78 -0
  14. data/lib/rubik/adapter/python.rb +30 -0
  15. data/lib/rubik/adapter/registry.rb +81 -0
  16. data/lib/rubik/adapter/ruby.rb +30 -0
  17. data/lib/rubik/adapter/rust.rb +36 -0
  18. data/lib/rubik/adapter/zig.rb +36 -0
  19. data/lib/rubik/adapter.rb +18 -0
  20. data/lib/rubik/ast/body_loader.rb +19 -0
  21. data/lib/rubik/ast/c_extractor.rb +114 -0
  22. data/lib/rubik/ast/cache_store.rb +191 -0
  23. data/lib/rubik/ast/extractor.rb +59 -0
  24. data/lib/rubik/ast/fallback_cleaner.rb +145 -0
  25. data/lib/rubik/ast/go_extractor.rb +148 -0
  26. data/lib/rubik/ast/graph.rb +272 -0
  27. data/lib/rubik/ast/python_extractor.rb +120 -0
  28. data/lib/rubik/ast/ruby_extractor.rb +260 -0
  29. data/lib/rubik/ast/rust_extractor.rb +127 -0
  30. data/lib/rubik/ast/symbol.rb +103 -0
  31. data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
  32. data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
  33. data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
  34. data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
  35. data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
  36. data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
  37. data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
  38. data/lib/rubik/ast/treesitter_extractor.rb +93 -0
  39. data/lib/rubik/ast/typescript_extractor.rb +283 -0
  40. data/lib/rubik/ast/zig_extractor.rb +116 -0
  41. data/lib/rubik/ast.rb +13 -0
  42. data/lib/rubik/benchmark/runner.rb +224 -0
  43. data/lib/rubik/benchmark/synth_repo.rb +59 -0
  44. data/lib/rubik/benchmark.rb +9 -0
  45. data/lib/rubik/buffer/diff.rb +43 -0
  46. data/lib/rubik/buffer/manager.rb +427 -0
  47. data/lib/rubik/buffer/path_validator.rb +60 -0
  48. data/lib/rubik/buffer/snapshot.rb +99 -0
  49. data/lib/rubik/buffer.rb +10 -0
  50. data/lib/rubik/coeffects.rb +129 -0
  51. data/lib/rubik/concurrency/boundary.rb +74 -0
  52. data/lib/rubik/concurrency/dns_pool.rb +68 -0
  53. data/lib/rubik/concurrency/errors.rb +17 -0
  54. data/lib/rubik/concurrency/event_loop.rb +441 -0
  55. data/lib/rubik/concurrency/join.rb +55 -0
  56. data/lib/rubik/concurrency/process_waiter.rb +55 -0
  57. data/lib/rubik/concurrency/scheduler.rb +84 -0
  58. data/lib/rubik/concurrency/timers.rb +109 -0
  59. data/lib/rubik/concurrency.rb +132 -0
  60. data/lib/rubik/config/deep_ops.rb +80 -0
  61. data/lib/rubik/config/defaults.rb +248 -0
  62. data/lib/rubik/config/model_catalog.rb +93 -0
  63. data/lib/rubik/config/store.rb +155 -0
  64. data/lib/rubik/config.rb +216 -0
  65. data/lib/rubik/context/git_info.rb +84 -0
  66. data/lib/rubik/context/git_lister.rb +37 -0
  67. data/lib/rubik/context/index.rb +134 -0
  68. data/lib/rubik/context/repo_context_reader.rb +121 -0
  69. data/lib/rubik/context/scanner.rb +439 -0
  70. data/lib/rubik/context.rb +11 -0
  71. data/lib/rubik/diagnostics/collector.rb +200 -0
  72. data/lib/rubik/diagnostics/entry.rb +60 -0
  73. data/lib/rubik/diagnostics/parser.rb +107 -0
  74. data/lib/rubik/diagnostics.rb +10 -0
  75. data/lib/rubik/event_bus.rb +56 -0
  76. data/lib/rubik/kernel/dispatcher.rb +171 -0
  77. data/lib/rubik/kernel/dmesg.rb +116 -0
  78. data/lib/rubik/kernel/init.rb +177 -0
  79. data/lib/rubik/kernel/record.rb +85 -0
  80. data/lib/rubik/kernel/unit.rb +160 -0
  81. data/lib/rubik/llm/anthropic.rb +202 -0
  82. data/lib/rubik/llm/chunk_assembler.rb +285 -0
  83. data/lib/rubik/llm/client.rb +526 -0
  84. data/lib/rubik/llm/formatter.rb +450 -0
  85. data/lib/rubik/llm/message_sanitizer.rb +87 -0
  86. data/lib/rubik/llm/ollama.rb +167 -0
  87. data/lib/rubik/llm/openai.rb +21 -0
  88. data/lib/rubik/llm/openai_compatible.rb +216 -0
  89. data/lib/rubik/llm/provider.rb +125 -0
  90. data/lib/rubik/llm/registry.rb +336 -0
  91. data/lib/rubik/llm/router.rb +100 -0
  92. data/lib/rubik/llm.rb +18 -0
  93. data/lib/rubik/log.rb +38 -0
  94. data/lib/rubik/lsp/client.rb +475 -0
  95. data/lib/rubik/lsp/manager.rb +366 -0
  96. data/lib/rubik/lsp.rb +9 -0
  97. data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
  98. data/lib/rubik/mcp/client.rb +169 -0
  99. data/lib/rubik/mcp/errors.rb +11 -0
  100. data/lib/rubik/mcp/manager.rb +619 -0
  101. data/lib/rubik/mcp/proxy_tool.rb +104 -0
  102. data/lib/rubik/mcp/transport/stdio.rb +255 -0
  103. data/lib/rubik/mcp.rb +12 -0
  104. data/lib/rubik/orchestrator/activity_log.rb +75 -0
  105. data/lib/rubik/orchestrator/auto_apply.rb +166 -0
  106. data/lib/rubik/orchestrator/budget_manager.rb +97 -0
  107. data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
  108. data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
  109. data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
  110. data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
  111. data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
  112. data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
  113. data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
  114. data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
  115. data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
  116. data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
  117. data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
  118. data/lib/rubik/orchestrator/goal_state.rb +184 -0
  119. data/lib/rubik/orchestrator/goal_wait.rb +125 -0
  120. data/lib/rubik/orchestrator/loop.rb +388 -0
  121. data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
  122. data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
  123. data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
  124. data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
  125. data/lib/rubik/orchestrator/shell_deps.rb +85 -0
  126. data/lib/rubik/orchestrator/step_executor.rb +328 -0
  127. data/lib/rubik/orchestrator/stream_manager.rb +118 -0
  128. data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
  129. data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
  130. data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
  131. data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
  132. data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
  133. data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
  134. data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
  135. data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
  136. data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
  137. data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
  138. data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
  139. data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
  140. data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
  141. data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
  142. data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
  143. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
  144. data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
  145. data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
  146. data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
  147. data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
  148. data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
  149. data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
  150. data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
  151. data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
  152. data/lib/rubik/orchestrator/tool_history.rb +39 -0
  153. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
  154. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
  155. data/lib/rubik/orchestrator/turn_executor.rb +376 -0
  156. data/lib/rubik/orchestrator.rb +11 -0
  157. data/lib/rubik/platform/filesystem.rb +64 -0
  158. data/lib/rubik/platform/io_drain.rb +53 -0
  159. data/lib/rubik/platform/path.rb +210 -0
  160. data/lib/rubik/platform/process.rb +38 -0
  161. data/lib/rubik/platform/sanitize.rb +157 -0
  162. data/lib/rubik/platform.rb +113 -0
  163. data/lib/rubik/plugin/dsl.rb +51 -0
  164. data/lib/rubik/plugin/manager.rb +247 -0
  165. data/lib/rubik/plugin/storage.rb +195 -0
  166. data/lib/rubik/plugin.rb +10 -0
  167. data/lib/rubik/prompt/context_builder.rb +377 -0
  168. data/lib/rubik/prompt/context_compactor.rb +326 -0
  169. data/lib/rubik/prompt/conversation_rows.rb +74 -0
  170. data/lib/rubik/prompt/doctrine.rb +160 -0
  171. data/lib/rubik/prompt/emergency_compactor.rb +81 -0
  172. data/lib/rubik/prompt/services.rb +60 -0
  173. data/lib/rubik/prompt/token_estimator.rb +63 -0
  174. data/lib/rubik/prompt/tool_teachings.rb +78 -0
  175. data/lib/rubik/prompt.rb +13 -0
  176. data/lib/rubik/repair/loop.rb +95 -0
  177. data/lib/rubik/repair.rb +8 -0
  178. data/lib/rubik/repl/ast_warmup.rb +129 -0
  179. data/lib/rubik/repl/command_context.rb +214 -0
  180. data/lib/rubik/repl/command_parser.rb +48 -0
  181. data/lib/rubik/repl/commands/agent.rb +30 -0
  182. data/lib/rubik/repl/commands/agent_context.rb +289 -0
  183. data/lib/rubik/repl/commands/agent_execution.rb +342 -0
  184. data/lib/rubik/repl/commands/agent_models.rb +212 -0
  185. data/lib/rubik/repl/commands/agent_providers.rb +232 -0
  186. data/lib/rubik/repl/commands/base.rb +55 -0
  187. data/lib/rubik/repl/commands/buffer.rb +124 -0
  188. data/lib/rubik/repl/commands/dmesg.rb +87 -0
  189. data/lib/rubik/repl/commands/lsp.rb +235 -0
  190. data/lib/rubik/repl/commands/mcp.rb +194 -0
  191. data/lib/rubik/repl/commands/plugins.rb +208 -0
  192. data/lib/rubik/repl/commands/pruner.rb +71 -0
  193. data/lib/rubik/repl/commands/session.rb +183 -0
  194. data/lib/rubik/repl/commands/skills.rb +111 -0
  195. data/lib/rubik/repl/commands/storage.rb +85 -0
  196. data/lib/rubik/repl/commands/subagents.rb +195 -0
  197. data/lib/rubik/repl/commands/system.rb +85 -0
  198. data/lib/rubik/repl/commands/tasks.rb +120 -0
  199. data/lib/rubik/repl/commands/workspace.rb +443 -0
  200. data/lib/rubik/repl/commands.rb +44 -0
  201. data/lib/rubik/repl/composition.rb +32 -0
  202. data/lib/rubik/repl/config_wiring.rb +142 -0
  203. data/lib/rubik/repl/confirmations.rb +56 -0
  204. data/lib/rubik/repl/hooks.rb +47 -0
  205. data/lib/rubik/repl/input_execution.rb +323 -0
  206. data/lib/rubik/repl/input_loop.rb +213 -0
  207. data/lib/rubik/repl/interrupt_handler.rb +217 -0
  208. data/lib/rubik/repl/lifecycle.rb +410 -0
  209. data/lib/rubik/repl/paste_support.rb +88 -0
  210. data/lib/rubik/repl/pending_notice_queue.rb +60 -0
  211. data/lib/rubik/repl/presentation.rb +132 -0
  212. data/lib/rubik/repl/pruner/brief.rb +101 -0
  213. data/lib/rubik/repl/pruner/gate.rb +279 -0
  214. data/lib/rubik/repl/pruner/harness.rb +236 -0
  215. data/lib/rubik/repl/pruner/rules.rb +316 -0
  216. data/lib/rubik/repl/pruner/service.rb +82 -0
  217. data/lib/rubik/repl/pruner.rb +39 -0
  218. data/lib/rubik/repl/service_catalog.rb +374 -0
  219. data/lib/rubik/repl/setup_factories.rb +66 -0
  220. data/lib/rubik/repl/shell.rb +231 -0
  221. data/lib/rubik/repl/step_formatter.rb +324 -0
  222. data/lib/rubik/repl/tee_io.rb +105 -0
  223. data/lib/rubik/repl/terminal_guard.rb +157 -0
  224. data/lib/rubik/repl/turn_recorder.rb +234 -0
  225. data/lib/rubik/repl.rb +14 -0
  226. data/lib/rubik/runtime/ast_service.rb +78 -0
  227. data/lib/rubik/runtime/context.rb +335 -0
  228. data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
  229. data/lib/rubik/runtime/discovery.rb +342 -0
  230. data/lib/rubik/runtime/fiber.rb +142 -0
  231. data/lib/rubik/runtime/fiber_ordering.rb +52 -0
  232. data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
  233. data/lib/rubik/runtime/index_service.rb +58 -0
  234. data/lib/rubik/runtime/loop_service.rb +73 -0
  235. data/lib/rubik/runtime/manager.rb +241 -0
  236. data/lib/rubik/runtime/overlay_storage.rb +481 -0
  237. data/lib/rubik/runtime/service_definition.rb +54 -0
  238. data/lib/rubik/runtime/service_registration.rb +33 -0
  239. data/lib/rubik/runtime/task_manager.rb +302 -0
  240. data/lib/rubik/runtime.rb +19 -0
  241. data/lib/rubik/sandbox/driver/base.rb +30 -0
  242. data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
  243. data/lib/rubik/sandbox/driver/docker.rb +113 -0
  244. data/lib/rubik/sandbox/driver/podman.rb +98 -0
  245. data/lib/rubik/sandbox/driver/process.rb +190 -0
  246. data/lib/rubik/sandbox/driver.rb +91 -0
  247. data/lib/rubik/sandbox/runner.rb +218 -0
  248. data/lib/rubik/sandbox/shell_resolver.rb +178 -0
  249. data/lib/rubik/sandbox.rb +8 -0
  250. data/lib/rubik/security/blocked_guidance.rb +187 -0
  251. data/lib/rubik/security/classifier/classifier.rb +440 -0
  252. data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
  253. data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
  254. data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
  255. data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
  256. data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
  257. data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
  258. data/lib/rubik/security/classifier/commands/black.yaml +12 -0
  259. data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
  260. data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
  261. data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
  262. data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
  263. data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
  264. data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
  265. data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
  266. data/lib/rubik/security/classifier/commands/column.yaml +7 -0
  267. data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
  268. data/lib/rubik/security/classifier/commands/command.yaml +12 -0
  269. data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
  270. data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
  271. data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
  272. data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
  273. data/lib/rubik/security/classifier/commands/date.yaml +10 -0
  274. data/lib/rubik/security/classifier/commands/df.yaml +7 -0
  275. data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
  276. data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
  277. data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
  278. data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
  279. data/lib/rubik/security/classifier/commands/du.yaml +7 -0
  280. data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
  281. data/lib/rubik/security/classifier/commands/env.yaml +15 -0
  282. data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
  283. data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
  284. data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
  285. data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
  286. data/lib/rubik/security/classifier/commands/false.yaml +7 -0
  287. data/lib/rubik/security/classifier/commands/file.yaml +7 -0
  288. data/lib/rubik/security/classifier/commands/find.yaml +25 -0
  289. data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
  290. data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
  291. data/lib/rubik/security/classifier/commands/free.yaml +7 -0
  292. data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
  293. data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
  294. data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
  295. data/lib/rubik/security/classifier/commands/git.yaml +524 -0
  296. data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
  297. data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
  298. data/lib/rubik/security/classifier/commands/go.yaml +44 -0
  299. data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
  300. data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
  301. data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
  302. data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
  303. data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
  304. data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
  305. data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
  306. data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
  307. data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
  308. data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
  309. data/lib/rubik/security/classifier/commands/head.yaml +7 -0
  310. data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
  311. data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
  312. data/lib/rubik/security/classifier/commands/id.yaml +7 -0
  313. data/lib/rubik/security/classifier/commands/install.yaml +7 -0
  314. data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
  315. data/lib/rubik/security/classifier/commands/java.yaml +35 -0
  316. data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
  317. data/lib/rubik/security/classifier/commands/join.yaml +7 -0
  318. data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
  319. data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
  320. data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
  321. data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
  322. data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
  323. data/lib/rubik/security/classifier/commands/less.yaml +7 -0
  324. data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
  325. data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
  326. data/lib/rubik/security/classifier/commands/make.yaml +21 -0
  327. data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
  328. data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
  329. data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
  330. data/lib/rubik/security/classifier/commands/more.yaml +7 -0
  331. data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
  332. data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
  333. data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
  334. data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
  335. data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
  336. data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
  337. data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
  338. data/lib/rubik/security/classifier/commands/node.yaml +20 -0
  339. data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
  340. data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
  341. data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
  342. data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
  343. data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
  344. data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
  345. data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
  346. data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
  347. data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
  348. data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
  349. data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
  350. data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
  351. data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
  352. data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
  353. data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
  354. data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
  355. data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
  356. data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
  357. data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
  358. data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
  359. data/lib/rubik/security/classifier/commands/python.yaml +9 -0
  360. data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
  361. data/lib/rubik/security/classifier/commands/read.yaml +7 -0
  362. data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
  363. data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
  364. data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
  365. data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
  366. data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
  367. data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
  368. data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
  369. data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
  370. data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
  371. data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
  372. data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
  373. data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
  374. data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
  375. data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
  376. data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
  377. data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
  378. data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
  379. data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
  380. data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
  381. data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
  382. data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
  383. data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
  384. data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
  385. data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
  386. data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
  387. data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
  388. data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
  389. data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
  390. data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
  391. data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
  392. data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
  393. data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
  394. data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
  395. data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
  396. data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
  397. data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
  398. data/lib/rubik/security/classifier/commands/test.yaml +7 -0
  399. data/lib/rubik/security/classifier/commands/time.yaml +8 -0
  400. data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
  401. data/lib/rubik/security/classifier/commands/top.yaml +7 -0
  402. data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
  403. data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
  404. data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
  405. data/lib/rubik/security/classifier/commands/true.yaml +7 -0
  406. data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
  407. data/lib/rubik/security/classifier/commands/type.yaml +7 -0
  408. data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
  409. data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
  410. data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
  411. data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
  412. data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
  413. data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
  414. data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
  415. data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
  416. data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
  417. data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
  418. data/lib/rubik/security/classifier/commands/which.yaml +7 -0
  419. data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
  420. data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
  421. data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
  422. data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
  423. data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
  424. data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
  425. data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
  426. data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
  427. data/lib/rubik/security/classifier/database.rb +216 -0
  428. data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
  429. data/lib/rubik/security/classifier/matcher/options.rb +245 -0
  430. data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
  431. data/lib/rubik/security/classifier/matcher.rb +249 -0
  432. data/lib/rubik/security/classifier/models.rb +170 -0
  433. data/lib/rubik/security/classifier/parser.rb +23 -0
  434. data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
  435. data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
  436. data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
  437. data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
  438. data/lib/rubik/security/classifier.rb +25 -0
  439. data/lib/rubik/security/errors.rb +39 -0
  440. data/lib/rubik/security/guard.rb +954 -0
  441. data/lib/rubik/security/redactor.rb +89 -0
  442. data/lib/rubik/security.rb +11 -0
  443. data/lib/rubik/session/manager.rb +331 -0
  444. data/lib/rubik/session/recorder.rb +149 -0
  445. data/lib/rubik/session/replayer.rb +65 -0
  446. data/lib/rubik/session.rb +10 -0
  447. data/lib/rubik/skills/frontmatter.rb +41 -0
  448. data/lib/rubik/skills/manager.rb +223 -0
  449. data/lib/rubik/skills/registry.rb +64 -0
  450. data/lib/rubik/skills/skill.rb +33 -0
  451. data/lib/rubik/skills/storage.rb +77 -0
  452. data/lib/rubik/skills/validator.rb +116 -0
  453. data/lib/rubik/skills.rb +20 -0
  454. data/lib/rubik/storage/folding.rb +23 -0
  455. data/lib/rubik/storage/layout.rb +145 -0
  456. data/lib/rubik/storage/render.rb +97 -0
  457. data/lib/rubik/storage/service.rb +42 -0
  458. data/lib/rubik/storage/store.rb +514 -0
  459. data/lib/rubik/storage/transcript_indexer.rb +116 -0
  460. data/lib/rubik/storage/uri_reader.rb +168 -0
  461. data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
  462. data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
  463. data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
  464. data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
  465. data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
  466. data/lib/rubik/storage/vakanuvis/service.rb +69 -0
  467. data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
  468. data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
  469. data/lib/rubik/storage/vakanuvis.rb +77 -0
  470. data/lib/rubik/storage.rb +29 -0
  471. data/lib/rubik/tools/base.rb +107 -0
  472. data/lib/rubik/tools/builtins/dmesg.rb +74 -0
  473. data/lib/rubik/tools/builtins/exec.rb +293 -0
  474. data/lib/rubik/tools/builtins/files.rb +354 -0
  475. data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
  476. data/lib/rubik/tools/builtins/grep.rb +281 -0
  477. data/lib/rubik/tools/builtins/interactive.rb +175 -0
  478. data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
  479. data/lib/rubik/tools/builtins/lsp.rb +283 -0
  480. data/lib/rubik/tools/builtins/pruner.rb +88 -0
  481. data/lib/rubik/tools/builtins/runtime.rb +120 -0
  482. data/lib/rubik/tools/builtins/send_message.rb +109 -0
  483. data/lib/rubik/tools/builtins/show_file.rb +84 -0
  484. data/lib/rubik/tools/builtins/skill.rb +72 -0
  485. data/lib/rubik/tools/builtins/storage.rb +232 -0
  486. data/lib/rubik/tools/builtins/subagent.rb +356 -0
  487. data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
  488. data/lib/rubik/tools/builtins/symbols.rb +209 -0
  489. data/lib/rubik/tools/builtins/synthesis.rb +314 -0
  490. data/lib/rubik/tools/builtins/task.rb +168 -0
  491. data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
  492. data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
  493. data/lib/rubik/tools/builtins.rb +30 -0
  494. data/lib/rubik/tools/catalog.rb +70 -0
  495. data/lib/rubik/tools/declaration.rb +90 -0
  496. data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
  497. data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
  498. data/lib/rubik/tools/native_search.rb +180 -0
  499. data/lib/rubik/tools/output.rb +54 -0
  500. data/lib/rubik/tools/preflight_runner.rb +209 -0
  501. data/lib/rubik/tools/registry.rb +247 -0
  502. data/lib/rubik/tools/result.rb +71 -0
  503. data/lib/rubik/tools/services.rb +96 -0
  504. data/lib/rubik/tools/truncator.rb +78 -0
  505. data/lib/rubik/tools/uri_resolver.rb +195 -0
  506. data/lib/rubik/tools.rb +14 -0
  507. data/lib/rubik/ui/approvals.rb +52 -0
  508. data/lib/rubik/ui/banner.rb +43 -0
  509. data/lib/rubik/ui/capability.rb +122 -0
  510. data/lib/rubik/ui/dialog.rb +106 -0
  511. data/lib/rubik/ui/diff_viewer.rb +78 -0
  512. data/lib/rubik/ui/frame.rb +95 -0
  513. data/lib/rubik/ui/markdown_stream.rb +107 -0
  514. data/lib/rubik/ui/panel.rb +98 -0
  515. data/lib/rubik/ui/renderer.rb +206 -0
  516. data/lib/rubik/ui/spinner.rb +120 -0
  517. data/lib/rubik/ui/statusline.rb +134 -0
  518. data/lib/rubik/ui/theme.rb +118 -0
  519. data/lib/rubik/ui.rb +57 -0
  520. data/lib/rubik/uris.rb +101 -0
  521. data/lib/rubik/version.rb +5 -0
  522. data/lib/rubik.rb +65 -0
  523. metadata +770 -0
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+ require "stringio"
6
+ require "timeout"
7
+
8
+ require_relative "../security/errors"
9
+
10
+ module Rubik
11
+ module Tools
12
+ # Runs synthesized handler code in a short-lived child ruby process with a
13
+ # hard kill deadline (AGENTS.md §3.4 Session Isolation).
14
+ #
15
+ # Why a subprocess: Timeout.timeout relies on Thread.raise, which cannot
16
+ # preempt C extensions or uninterruptible blocking syscalls — a runaway
17
+ # handler could hang the whole REPL past its budget. A child process can
18
+ # always be killed. When spawning is impossible (exotic embeddings, strict
19
+ # sandboxes) the runner reports :spawn_failed so callers can fall back to
20
+ # the in-process Timeout path.
21
+ module PreflightRunner
22
+ DEFAULT_TIMEOUT = 5
23
+
24
+ # Child evaluator: reads {code:, args:, mode:} from stdin, calls the
25
+ # handler, prints a single JSON verdict line. Single-quoted heredoc —
26
+ # never interpolated into the parent's source.
27
+ CHILD_SCRIPT = <<~'RUBY'
28
+ require "json"
29
+ require "stringio"
30
+ begin
31
+ payload = JSON.parse($stdin.read)
32
+ handler = eval(payload["code"], Object.new.instance_eval { binding }, __FILE__, __LINE__)
33
+ raise "code did not evaluate to a callable Proc" unless handler.respond_to?(:call)
34
+ # 2-arity command handlers receive the live CommandContext at
35
+ # dispatch; preflight mirrors its OUTPUT VIEW (out/puts/print/
36
+ # flush delegate to a throwaway StringIO) without the repl/
37
+ # require. Service reads fail loudly here — such handlers should
38
+ # declare 3-arity and be verified with the raw shell instead.
39
+ command_view = Struct.new(:out) do
40
+ def puts(*args)
41
+ out.puts(*args)
42
+ end
43
+
44
+ def print(*args)
45
+ out.print(*args)
46
+ end
47
+
48
+ def flush
49
+ out.flush if out.respond_to?(:flush)
50
+ end
51
+ end
52
+ case payload["mode"]
53
+ when "tool"
54
+ handler.call(payload["args"])
55
+ else
56
+ view = command_view.new(StringIO.new)
57
+ case handler.arity
58
+ when 1 then handler.call(payload["args"])
59
+ when 2 then handler.call(payload["args"], view)
60
+ else handler.call(payload["args"], view.out, nil)
61
+ end
62
+ end
63
+ STDOUT.puts JSON.dump(ok: true)
64
+ rescue StandardError, ScriptError => e
65
+ STDOUT.puts JSON.dump(ok: false, error: "#{e.class}: #{e.message}")
66
+ end
67
+ RUBY
68
+
69
+ module_function
70
+
71
+ # Executes +code+ (a self-contained lambda) with +args+ in an isolated
72
+ # child process. mode: :tool mirrors the ToolWrapper contract (exactly
73
+ # one argument); :command mirrors the REPL arity dispatch (args, out,
74
+ # nil shell — the real shell is unavailable in the child).
75
+ #
76
+ # The child process has no Rubik loaded, so payload["code"] cannot be
77
+ # verified there — the ONLY verification it can rely on is whatever
78
+ # the parent proved BEFORE spawning. guard is a REQUIRED kwarg on
79
+ # purpose: a future caller cannot skip the spawn-boundary check
80
+ # silently (ArgumentError at the call site).
81
+ #
82
+ # Returns nil on success, an error-detail String on failure, or
83
+ # :spawn_failed when the child infrastructure is unusable.
84
+ def run(code, args, guard:, mode: :command, timeout: DEFAULT_TIMEOUT)
85
+ # Shared gate at the spawn boundary: the duck-typed downgrade (a
86
+ # test-double affordance — production guards always define
87
+ # verify_synthesis_safety!) warns loudly at the single seam instead
88
+ # of silently skipping the AST deny surface here too.
89
+ Rubik::Security::Guard.run_synthesis_gate(guard, code)
90
+ payload = JSON.dump(code: code, args: args || {}, mode: mode.to_s)
91
+ run_subprocess(payload, timeout)
92
+ rescue Rubik::Security::BlockedWriteError => e
93
+ # Must precede the generic rescue: BlockedWriteError is a
94
+ # StandardError, and a block verdict downgraded to :spawn_failed
95
+ # would make the caller's in-process fallback run the blocked code.
96
+ "security verification failed: #{e.message}"
97
+ rescue TypeError, JSON::GeneratorError, SystemCallError
98
+ # Non-JSON-serializable args (TypeError/GeneratorError) or any other
99
+ # payload problem — caller falls back to in-process.
100
+ :spawn_failed
101
+ end
102
+
103
+ # In-process fallback (subprocess unavailable). Yields the caller's
104
+ # verification block under a Timeout guard. Returns nil on success or
105
+ # an error-detail String. Same caveat as before: Timeout cannot preempt
106
+ # C extensions or uninterruptible syscalls.
107
+ def verify_in_process(timeout: DEFAULT_TIMEOUT, &)
108
+ Timeout.timeout(timeout, &)
109
+ nil
110
+ rescue Timeout::Error
111
+ "execution timed out after #{timeout} seconds"
112
+ rescue StandardError, ScriptError => e
113
+ "#{e.class}: #{e.message}"
114
+ end
115
+
116
+ # Parent-side mirror of the child evaluator's dispatch (see CHILD_SCRIPT).
117
+ # Keeps the fallback path semantically identical to the subprocess path.
118
+ def call_handler(handler, args, mode: :command, shell: nil)
119
+ if mode == :tool
120
+ handler.call(args)
121
+ else
122
+ command_view = Struct.new(:out) do
123
+ def puts(*)
124
+ out.puts(*)
125
+ end
126
+
127
+ def print(*)
128
+ out.print(*)
129
+ end
130
+
131
+ def flush
132
+ out.flush if out.respond_to?(:flush)
133
+ end
134
+ end
135
+ view = command_view.new(StringIO.new)
136
+ case handler.arity
137
+ when 1 then handler.call(args)
138
+ when 2 then handler.call(args, view)
139
+ else handler.call(args, view.out, shell)
140
+ end
141
+ end
142
+ end
143
+
144
+ def run_subprocess(payload, timeout)
145
+ rd_in = wr_in = rd_out = wr_out = nil
146
+ rd_in, wr_in = IO.pipe
147
+ rd_out, wr_out = IO.pipe
148
+ pid = Process.spawn(RbConfig.ruby, "-e", CHILD_SCRIPT, in: rd_in, out: wr_out, err: File::NULL)
149
+ rd_in.close
150
+ wr_out.close
151
+ wr_in.write(payload)
152
+ wr_in.close
153
+
154
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
155
+ output = +""
156
+ loop do
157
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
158
+ if remaining <= 0
159
+ kill_child(pid)
160
+ return "execution timed out after #{timeout} seconds (subprocess killed)"
161
+ end
162
+ # wait_readable returning false (deadline slice elapsed) must NOT
163
+ # break: the deadline check above is what kills a runaway child.
164
+ next unless rd_out.wait_readable(remaining)
165
+
166
+ chunk = rd_out.read_nonblock(4096, exception: false)
167
+ break if chunk.nil? # EOF — child finished
168
+ next if chunk == :wait_readable
169
+
170
+ output << chunk
171
+ end
172
+ begin
173
+ Process.wait(pid)
174
+ rescue StandardError
175
+ nil
176
+ end
177
+ parse_verdict(output)
178
+ rescue StandardError
179
+ kill_child(pid) if defined?(pid) && pid
180
+ :spawn_failed
181
+ ensure
182
+ [rd_in, wr_in, rd_out, wr_out].each do |io|
183
+ io.close unless io.nil? || io.closed?
184
+ rescue StandardError
185
+ nil
186
+ end
187
+ end
188
+
189
+ def kill_child(pid)
190
+ Process.kill("KILL", pid)
191
+ Process.wait(pid)
192
+ rescue StandardError
193
+ nil
194
+ end
195
+
196
+ def parse_verdict(output)
197
+ line = output.lines.reverse.find { |l| l.start_with?("{") }
198
+ return "subprocess produced no verdict" unless line
199
+
200
+ verdict = JSON.parse(line)
201
+ return nil if verdict["ok"]
202
+
203
+ verdict["error"].to_s
204
+ rescue JSON::ParserError
205
+ "subprocess produced unreadable output"
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "base"
5
+ require_relative "services"
6
+ require_relative "../coeffects"
7
+
8
+ module Rubik
9
+ module Tools
10
+ class Registry
11
+ def initialize(coeffect_ctx: nil)
12
+ @tools = {}
13
+ @aliases = {}
14
+ @tool_aliases = {}
15
+ @origins = {}
16
+ @coeffect_ctx = coeffect_ctx
17
+ @monitor = Monitor.new
18
+ end
19
+
20
+ # origin labels where a tool came from (:builtin, :overlay, :mcp,
21
+ # :synthesis). The synthesizer uses it to refuse shadowing built-ins
22
+ # while still allowing hot-updates of non-builtin tools.
23
+ def register(tool_instance, aliases: [], origin: :builtin)
24
+ @monitor.synchronize do
25
+ @tools[tool_instance.name] = tool_instance
26
+ @origins[tool_instance.name] = origin
27
+ all_aliases = (tool_instance.respond_to?(:aliases) ? Array(tool_instance.aliases) : []) + Array(aliases)
28
+ @tool_aliases[tool_instance.name] ||= []
29
+ all_aliases.each do |al|
30
+ clean_al = al.to_s.downcase
31
+ @aliases[clean_al] = tool_instance.name
32
+ @tool_aliases[tool_instance.name] << clean_al
33
+ end
34
+ tool_instance
35
+ end
36
+ end
37
+
38
+ def unregister(name)
39
+ @monitor.synchronize do
40
+ canonical_name = @tools.key?(name.to_s) ? name.to_s : @aliases[name.to_s.downcase]
41
+ removed_tool = @tools.delete(canonical_name) if canonical_name
42
+ @origins.delete(canonical_name) if canonical_name
43
+ if canonical_name && @tool_aliases[canonical_name]
44
+ @tool_aliases.delete(canonical_name).each do |al|
45
+ @aliases.delete(al) if @aliases[al] == canonical_name
46
+ end
47
+ else
48
+ @aliases.delete_if { |_k, v| v == canonical_name || v == name.to_s }
49
+ end
50
+ @aliases.delete(name.to_s.downcase) if @aliases[name.to_s.downcase] == canonical_name
51
+ removed_tool
52
+ end
53
+ end
54
+
55
+ # Atomic hot-swap of an existing tool with a new tool instance.
56
+ # Reentrant lock safety: Monitor#synchronize is explicitly reentrant (recursive),
57
+ # meaning the calling thread can safely enter unregister and register without deadlock.
58
+ def swap(name, new_tool, origin: :builtin)
59
+ @monitor.synchronize do
60
+ unregister(name)
61
+ register(new_tool, origin: origin) if new_tool
62
+ end
63
+ end
64
+
65
+ def get(tool_name)
66
+ @monitor.synchronize do
67
+ @tools[tool_name.to_s] || begin
68
+ canonical = @aliases[tool_name.to_s.downcase]
69
+ canonical ? @tools[canonical] : nil
70
+ end
71
+ end
72
+ end
73
+
74
+ # Registration origin of the tool resolved by name (canonical or alias),
75
+ # or nil when no such tool is registered.
76
+ def origin(name)
77
+ @monitor.synchronize do
78
+ canonical = @tools.key?(name.to_s) ? name.to_s : @aliases[name.to_s.downcase]
79
+ canonical ? @origins[canonical] : nil
80
+ end
81
+ end
82
+
83
+ def list
84
+ @monitor.synchronize do
85
+ @tools.values.dup
86
+ end
87
+ end
88
+
89
+ # Schemas of tools whose declared coeffects (REQUIRES) are currently
90
+ # satisfied — the toolbelt-side of the fiber state machine (AGENTS.md
91
+ # §2): a tool missing its coeffects sits PENDING (hidden from the LLM)
92
+ # and activates reactively when a provider supplies the service.
93
+ # Called per agent step, so the filter re-evaluates live. Tools remain
94
+ # invocable via Registry#get for graceful self-correction errors.
95
+ def schemas
96
+ @monitor.synchronize do
97
+ @tools.values.select { |t| coeffects_satisfied?(t) }.map(&:to_h)
98
+ end
99
+ end
100
+
101
+ def aliases_map
102
+ @monitor.synchronize do
103
+ @aliases.dup
104
+ end
105
+ end
106
+
107
+ # Service slots (guard, context_index, adapter_registry, ast_graph,
108
+ # task_manager, sandbox_runner, subagent_supervisor) resolve through
109
+ # the house triple chain in Tools::Services — explicit kwarg (the
110
+ # **service_overrides above), then the runtime coeffect, then a fresh
111
+ # standalone default — so standalone/test constructions stay
112
+ # fail-closed. Unknown service kwargs raise ArgumentError, naming the
113
+ # valid slots.
114
+ def self.build_default(
115
+ buffer_manager:, shell: nil, overlay_storage: nil, runtime_manager: nil,
116
+ lsp_manager: nil, config: nil, root_dir: Dir.pwd, **service_overrides
117
+ )
118
+ ctx = runtime_manager&.context
119
+ registry = new(coeffect_ctx: ctx.respond_to?(:satisfied?) ? ctx : nil)
120
+ native_switch = config || shell&.config
121
+ svc = Services.new(ctx: ctx, overrides: service_overrides, root_dir: root_dir, native_switch: native_switch)
122
+ # Raw kwarg, NOT svc.context_index — preserved verbatim from the
123
+ # pre-catalog registry: RunTestsTool takes the caller-supplied (and
124
+ # possibly nil) index while list/grep tools take the resolved one.
125
+ raw_context_index = service_overrides[:context_index]
126
+
127
+ register_file_and_search_tools(registry, buffer_manager: buffer_manager, svc: svc, root_dir: root_dir, native_switch: native_switch, ctx: ctx)
128
+ register_lsp_tools(registry, lsp_manager: lsp_manager, buffer_manager: buffer_manager)
129
+ register_execution_tools(registry, svc: svc, raw_context_index: raw_context_index, root_dir: root_dir, native_switch: native_switch, ctx: ctx)
130
+ register_repl_and_synthesis_tools(
131
+ registry,
132
+ shell: shell,
133
+ overlay_storage: overlay_storage,
134
+ runtime_manager: runtime_manager,
135
+ lsp_manager: lsp_manager,
136
+ svc: svc,
137
+ native_switch: native_switch,
138
+ root_dir: root_dir
139
+ )
140
+ register_buffer_and_ui_tools(registry, buffer_manager: buffer_manager, shell: shell, ctx: ctx)
141
+ register_subagent_tools(registry, svc: svc, ctx: ctx)
142
+ register_system_tools(registry, svc: svc, native_switch: native_switch, root_dir: root_dir, ctx: ctx)
143
+
144
+ overlay_storage&.load_tools(registry)
145
+ # Inverse-by-construction (AGENTS.md §1.A): one monoidal inverse for
146
+ # the whole registry — context teardown unregisters every tool in
147
+ # strict LIFO order together with the rest of the session effects.
148
+ ctx&.register_disposable { registry.list.each { |t| registry.unregister(t.name) } } if ctx.respond_to?(:register_disposable)
149
+ registry
150
+ end
151
+
152
+ def self.register_file_and_search_tools(registry, buffer_manager:, svc:, root_dir:, native_switch:, ctx:)
153
+ registry.register(ReadFileTool.new(buffer_manager, runtime_context: ctx))
154
+ registry.register(ReplaceFileContentTool.new(buffer_manager))
155
+ registry.register(WriteFileTool.new(buffer_manager))
156
+ registry.register(ListFilesTool.new(svc.context_index, root_dir: root_dir, runtime_context: ctx))
157
+ registry.register(GrepSearchTool.new(buffer_manager, svc.context_index, root_dir: root_dir, runtime_context: ctx, config: native_switch))
158
+ registry.register(SearchSymbolsTool.new(svc.ast_graph, runtime_context: ctx))
159
+ registry.register(GetOutlineTool.new(svc.ast_graph, runtime_context: ctx))
160
+ registry.register(FindReferencesTool.new(svc.ast_graph, runtime_context: ctx))
161
+ registry.register(GetSymbolTool.new(svc.ast_graph, runtime_context: ctx))
162
+ end
163
+
164
+ def self.register_lsp_tools(registry, lsp_manager:, buffer_manager:)
165
+ registry.register(LSPDiagnosticsTool.new(lsp_manager, buffer_manager: buffer_manager))
166
+ registry.register(LSPHoverTool.new(lsp_manager, buffer_manager: buffer_manager))
167
+ registry.register(LSPDefinitionTool.new(lsp_manager, buffer_manager: buffer_manager))
168
+ end
169
+
170
+ def self.register_execution_tools(registry, svc:, raw_context_index:, root_dir:, native_switch:, ctx:)
171
+ registry.register(RunTestsTool.new(svc.adapter_registry, raw_context_index, root_dir: root_dir,
172
+ config: native_switch, guard: svc.guard))
173
+ registry.register(RunCommandTool.new(
174
+ guard: svc.guard,
175
+ runner: svc.sandbox_runner,
176
+ root_dir: root_dir,
177
+ task_manager: svc.task_manager,
178
+ runtime_context: ctx
179
+ ))
180
+ registry.register(ManageTaskTool.new(task_manager: svc.task_manager, guard: svc.guard, runtime_context: ctx))
181
+ end
182
+
183
+ def self.register_repl_and_synthesis_tools(registry, shell:, overlay_storage:, runtime_manager:, lsp_manager:, svc:, native_switch:, root_dir:)
184
+ registry.register(ExecReplCommandTool.new(shell))
185
+ registry.register(SynthesizeCommandTool.new(shell, overlay_storage, svc.guard, runtime_manager: runtime_manager))
186
+ registry.register(SynthesizeToolTool.new(registry, overlay_storage, svc.guard, runtime_manager: runtime_manager))
187
+ registry.register(UnsynthesizeTool.new(shell, registry, overlay_storage, runtime_manager: runtime_manager))
188
+ registry.register(InspectRuntimeTool.new(runtime_manager, registry, shell))
189
+ registry.register(ProvideServiceTool.new(runtime_manager))
190
+ registry.register(RepairRuntimeTool.new(
191
+ runtime_manager: runtime_manager,
192
+ lsp_manager: lsp_manager,
193
+ adapter_registry: svc.adapter_registry,
194
+ overlay_storage: overlay_storage,
195
+ config: native_switch,
196
+ root_dir: root_dir,
197
+ guard: svc.guard
198
+ ))
199
+ end
200
+
201
+ def self.register_buffer_and_ui_tools(registry, buffer_manager:, shell:, ctx:)
202
+ registry.register(ApplyBufferTool.new(buffer_manager, shell))
203
+ registry.register(AskQuestionTool.new(shell: shell, runtime_context: ctx))
204
+ end
205
+
206
+ def self.register_subagent_tools(registry, svc:, ctx:)
207
+ registry.register(InvokeSubagentTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
208
+ registry.register(ManageSubagentsTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
209
+ registry.register(SendMessageTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
210
+ registry.register(DefineSubagentTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
211
+ end
212
+
213
+ def self.register_system_tools(registry, svc:, native_switch:, root_dir:, ctx:)
214
+ registry.register(DmesgTool.new(svc.dmesg, runtime_context: ctx))
215
+ registry.register(StorageTool.new(nil, runtime_context: ctx, config: native_switch, root_dir: root_dir))
216
+ registry.register(VakanuvisTool.new(runtime_context: ctx))
217
+ registry.register(PruneContextTool.new(runtime_context: ctx))
218
+ registry.register(UpdateGoalLedgerTool.new(runtime_context: ctx))
219
+ registry.register(ShowFileTool.new(runtime_context: ctx))
220
+ registry.register(FinishTool.new)
221
+ end
222
+
223
+ private_class_method :register_file_and_search_tools,
224
+ :register_lsp_tools,
225
+ :register_execution_tools,
226
+ :register_repl_and_synthesis_tools,
227
+ :register_buffer_and_ui_tools,
228
+ :register_subagent_tools,
229
+ :register_system_tools
230
+
231
+ private
232
+
233
+ # A tool is satisfied when it declares no REQUIRES, when no coeffect
234
+ # context is wired (plain registries keep advertising everything), or
235
+ # when the context chain satisfies every declared dependency.
236
+ def coeffects_satisfied?(tool)
237
+ return true unless tool.class.const_defined?(:REQUIRES)
238
+
239
+ deps = tool.class.const_get(:REQUIRES)
240
+ return true if deps.nil? || deps.empty?
241
+ return true unless @coeffect_ctx.respond_to?(:satisfied?)
242
+
243
+ @coeffect_ctx.satisfied?(deps)
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+
5
+ module Rubik
6
+ module Tools
7
+ class Result
8
+ attr_reader :tool_name, :call_id, :status, :output, :error, :exit_code, :duration_ms, :truncated, :metadata
9
+
10
+ def initialize(tool_name:, call_id: nil, status: :ok, output: "", error: nil, exit_code: nil, duration_ms: 0.0, truncated: false, metadata: {})
11
+ @tool_name = tool_name.to_s
12
+ @call_id = call_id.to_s
13
+ @status = status.to_sym
14
+ @output = output.to_s
15
+ @error = error
16
+ @exit_code = exit_code
17
+ @duration_ms = duration_ms.to_f.round(1)
18
+ @truncated = truncated ? true : false
19
+ @metadata = metadata || {}
20
+ end
21
+
22
+ def ok?
23
+ @status == :ok
24
+ end
25
+
26
+ def error?
27
+ @status == :error
28
+ end
29
+
30
+ # Returns the tool result output as a plain string.
31
+ #
32
+ # @return [String] the output content of this tool result
33
+ def to_s
34
+ @output
35
+ end
36
+
37
+ # XML envelope for the conversation history. Both attributes and the
38
+ # output body are escaped: tool output is untrusted content (file
39
+ # contents, command output) and an unescaped "</tool_result>" or XML
40
+ # tag inside it could break the envelope or smuggle instructions into
41
+ # the prompt.
42
+ def to_xml
43
+ esc = Rubik::Platform.method(:xml_escape)
44
+ attrs = [
45
+ %(tool="#{esc.call(@tool_name)}"),
46
+ @call_id && !@call_id.empty? ? %(id="#{esc.call(@call_id)}") : nil,
47
+ %(status="#{@status}"),
48
+ @exit_code ? %(exit_code="#{@exit_code}") : nil,
49
+ %(duration_ms="#{@duration_ms}"),
50
+ @truncated ? %(truncated="true") : nil
51
+ ].compact.join(" ")
52
+
53
+ "<tool_result #{attrs}>\n#{esc.call(@output)}\n</tool_result>"
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ tool: @tool_name,
59
+ call_id: @call_id,
60
+ status: @status,
61
+ output: @output,
62
+ error: @error,
63
+ exit_code: @exit_code,
64
+ duration_ms: @duration_ms,
65
+ truncated: @truncated,
66
+ metadata: @metadata
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../coeffects"
4
+ require_relative "../kernel/dmesg" unless defined?(Rubik::Kernel::Dmesg)
5
+
6
+ module Rubik
7
+ module Tools
8
+ # Lazy per-key resolution view for the tool layer (architecture-review
9
+ # Candidate 05): ONE home for the house triple chain — explicit override
10
+ # kwarg, then the runtime coeffect (Coeffects.get), then a fresh
11
+ # standalone default. Collapses Registry.build_default's hand-written
12
+ # effective_* locals into a tier table.
13
+ #
14
+ # owns : the resolution tiers per service and the fresh-default
15
+ # table — standalone construction policy: a zero-policy Guard
16
+ # and a guard-chained sandbox Runner keep toolbelt-only
17
+ # builds fail-closed (never wider policy than the caller gave)
18
+ # leaves : key STRINGS (Rubik::Coeffects SSOT); full-graph construction
19
+ # (REPL::ServiceCatalog — the fresh tier is deliberately the
20
+ # DEGENERATE construction, not Composition's); tool
21
+ # constructor signatures (tools layer)
22
+ #
23
+ # The coeffect tier exists exactly where wire_runtime_context publishes
24
+ # the key (guard, context_index, adapter_registry, ast_graph,
25
+ # task_manager, sandbox, subagent supervisor), so a shell-built registry
26
+ # always resolves the session's own instances. ORDER is load-bearing:
27
+ # sandbox_runner's fresh default chains onto the RESOLVED guard, so
28
+ # :guard resolves first.
29
+ class Services
30
+ # Hash literal order IS resolution order: :guard resolves first because
31
+ # sandbox_runner's fresh default chains onto the RESOLVED guard. ORDER
32
+ # derives from the keys so the reader methods and the validation
33
+ # message can never drift from the table.
34
+ COEFFECTS = {
35
+ guard: Rubik::Coeffects::GUARD,
36
+ context_index: Rubik::Coeffects::CONTEXT_INDEX,
37
+ adapter_registry: Rubik::Coeffects::ADAPTER_REGISTRY,
38
+ ast_graph: Rubik::Coeffects::AST_GRAPH,
39
+ task_manager: Rubik::Coeffects::TASK_MANAGER,
40
+ sandbox_runner: Rubik::Coeffects::SANDBOX_RUNNER,
41
+ subagent_supervisor: Rubik::Coeffects::SUBAGENT_SUPERVISOR,
42
+ dmesg: Rubik::Coeffects::DMESG
43
+ }.freeze
44
+
45
+ ORDER = COEFFECTS.keys.freeze
46
+
47
+ FRESH = {
48
+ guard: ->(_s) { Rubik::Security::Guard.new },
49
+ context_index: ->(s) { Rubik::Context::Index.new(root_dir: s.root_dir, native: s.native_switch&.fetch("tools.native")) },
50
+ adapter_registry: ->(_s) { Rubik::Adapter::Registry.new },
51
+ ast_graph: ->(_s) { Rubik::AST::Graph.new },
52
+ sandbox_runner: ->(s) { Rubik::Sandbox::Runner.new(root_dir: s.root_dir, guard: s.resolved_guard) },
53
+ dmesg: ->(_s) { Rubik::Kernel::Dmesg.new }
54
+ }.freeze
55
+
56
+ attr_reader :root_dir, :native_switch
57
+
58
+ def initialize(ctx: nil, overrides: {}, root_dir: Dir.pwd, native_switch: nil)
59
+ unknown = overrides.keys - ORDER
60
+ raise ArgumentError, "unknown service override(s) #{unknown.inspect}; valid slots: #{ORDER.inspect}" unless unknown.empty?
61
+
62
+ @ctx = ctx
63
+ @overrides = overrides
64
+ @root_dir = root_dir
65
+ @native_switch = native_switch
66
+ @resolved = {}
67
+ end
68
+
69
+ ORDER.each do |slot|
70
+ define_method(slot) { resolve(slot) }
71
+ end
72
+
73
+ # The guard the sandbox fresh default chains onto (resolved first by
74
+ # ORDER). Public for the FRESH lambdas and for the tier tests.
75
+ def resolved_guard
76
+ resolve(:guard)
77
+ end
78
+
79
+ private
80
+
81
+ # The triple chain, memoized per slot: explicit override, then the
82
+ # runtime coeffect (canonical key first, legacy alias second via
83
+ # Coeffects.get), then the fresh standalone default. Slots without a
84
+ # tier (task_manager, subagent_supervisor) resolve to nil when neither
85
+ # the override nor the context supplies one — matching the registry's
86
+ # pre-catalog behavior.
87
+ def resolve(name)
88
+ return @resolved[name] if @resolved.key?(name)
89
+
90
+ @resolved[name] = @overrides[name] ||
91
+ (@ctx && Rubik::Coeffects.get(@ctx, COEFFECTS.fetch(name))) ||
92
+ FRESH[name]&.call(self)
93
+ end
94
+ end
95
+ end
96
+ end