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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../shell_resolver"
5
+ require_relative "../../security/guard" unless defined?(Rubik::Security::Guard)
6
+ require "open3"
7
+ require "timeout"
8
+
9
+ module Rubik
10
+ module Sandbox
11
+ module Driver
12
+ class ProcessDriver < Base
13
+ def available?
14
+ true
15
+ end
16
+
17
+ def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0") # rubocop:disable Lint/UnusedMethodArgument -- sürücü arayüzü kwarg'leri korunuyor
18
+ start_time = Time.now
19
+ stdout_str = ""
20
+ stderr_str = ""
21
+ exit_code = 127
22
+
23
+ # Explicit-argv spawn (Plan 55 D-1): never leave interpreter choice
24
+ # to Open3's string form — on Windows that silently means cmd.exe.
25
+ resolved = ShellResolver.resolve(cmd, preference: @options[:shell] || "auto")
26
+
27
+ env = {}
28
+ # Direct host execution does NOT inject misleading dead proxy variables (127.0.0.1:9999).
29
+ # Hardware/namespace network isolation is only provided when a true sandbox
30
+ # (Bubblewrap, Docker, Podman) is enabled.
31
+
32
+ pid = nil
33
+ begin
34
+ Timeout.timeout(timeout) do
35
+ Open3.popen3(env, *spawn_args_for(resolved, cmd), chdir: chdir) do |stdin, stdout, stderr, wait_thr|
36
+ pid = wait_thr.pid
37
+ begin
38
+ stdin.close
39
+ rescue
40
+ nil
41
+ end
42
+ t_out = Thread.new do
43
+ stdout.read
44
+ rescue
45
+ ""
46
+ end
47
+ t_err = Thread.new do
48
+ stderr.read
49
+ rescue
50
+ ""
51
+ end
52
+ begin
53
+ stdout_str = t_out.value.to_s
54
+ stderr_str = t_err.value.to_s
55
+ status = wait_thr.value
56
+ exit_code = status.exitstatus || (status.signaled? ? (128 + (status.termsig || 1)) : 1)
57
+ ensure
58
+ begin
59
+ t_out.kill
60
+ rescue
61
+ nil
62
+ end
63
+ begin
64
+ t_err.kill
65
+ rescue
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ rescue Timeout::Error
72
+ Rubik::Platform.kill_tree(pid) if pid
73
+ stderr_str = "Execution timed out after #{timeout} seconds"
74
+ exit_code = 124
75
+ rescue StandardError => e
76
+ stderr_str = "Execution error: #{e.message}"
77
+ exit_code = 1
78
+ end
79
+
80
+ duration = Time.now - start_time
81
+ stderr_out = resolved[:note] ? "#{resolved[:note]}\n#{stderr_str}" : stderr_str
82
+ require_relative "../../security/redactor" unless defined?(Rubik::Security::Redactor)
83
+ clean_stdout = Rubik::Security::Redactor.redact(stdout_str)
84
+ clean_stderr = Rubik::Security::Redactor.redact(stderr_out)
85
+ {
86
+ ok: exit_code.zero?,
87
+ exit_code: exit_code,
88
+ stdout: clean_stdout,
89
+ stderr: clean_stderr,
90
+ output: [clean_stdout, clean_stderr].reject(&:empty?).join("\n"),
91
+ duration: duration.round(4),
92
+ driver: "process",
93
+ shell: resolved[:shell]
94
+ }
95
+ end
96
+
97
+ private
98
+
99
+ # cmd.exe parses its tail with its own quoting rules that standard
100
+ # Windows argv quoting cannot express, so the command must reach it
101
+ # as a bare string (Ruby's string spawn form routes exactly there).
102
+ # bash/sh/pwsh take clean argv arrays.
103
+ #
104
+ # DEFENSE-IN-DEPTH (this closure, not a residual gap): the classifier
105
+ # runs in the ProcessDriver guard (lib/rubik/security/guard) and gates
106
+ # every spawn BEFORE this method is called, so a dangerous command
107
+ # never reaches spawn. But the classifier in the guard classifies the
108
+ # RAW `cmd` string with POSIX Shellwords and a nil shell. When the
109
+ # resolved shell is "cmd" cmd.exe re-parses the tail with its own
110
+ # grammar, so a command the classifier deemed "safe" under POSIX could
111
+ # be re-interpreted (e.g. the cmd builtin `start http://x && evil.exe`
112
+ # — `start` opens a URL, then `&&` chains the second program). To close
113
+ # that split we (1) re-run the guard's classification with the cmd shell
114
+ # hint right here, BEFORE spawning, and (2) explicitly block the cmd.exe
115
+ # builtins and drive-letter destructive constructs that the POSIX
116
+ # classifier can misread as safe. A POSIX-safe command that is
117
+ # dangerous under cmd.exe is therefore rejected and never spawned.
118
+ def spawn_args_for(resolved, cmd)
119
+ resolved[:shell] == "cmd" ? verify_cmd_safe(cmd) : resolved[:argv]
120
+ end
121
+
122
+ # Guard the cmd.exe branch. Re-classifies with shell: "cmd" so the guard
123
+ # applies cmd.exe semantics (builtins, quoting) rather than POSIX ones,
124
+ # and explicitly blocks the builtins/drive constructs the POSIX
125
+ # classifier can misread as safe. A command POSIX-safe but
126
+ # cmd.exe-dangerous must NOT be spawned.
127
+ def verify_cmd_safe(cmd)
128
+ # SAFETY ORDER (critical): the global classifier/allowlist/fatal-regex
129
+ # check MUST run BEFORE any cmd-specific approval. verify_command_safety!
130
+ # gates every spawn on the operator allowlist and the catastrophic
131
+ # fallback regex, so nothing the classifier rejects can ever reach the
132
+ # spawn path — even though the cmd-specific check below could approve
133
+ # it. Reversing this order would let a cmd-specific construct bypass
134
+ # the allowlist (HIGH finding). verify_cmd_specific_safe! is only a
135
+ # late-stage refinement: it may tighten an already-approved command,
136
+ # never authorize one the classifier rejected.
137
+ verify_command_safety!(cmd, shell: "cmd")
138
+ verify_cmd_specific_safe!(cmd)
139
+ [cmd]
140
+ end
141
+
142
+ # cmd.exe builtins / constructs that POSIX Shellwords would treat as a
143
+ # harmless word or redirect but cmd.exe executes with special (often
144
+ # dangerous) meaning. Guarded behind windows? so non-Windows hosts (where
145
+ # a cmd branch never resolves) are unaffected.
146
+ CMD_DANGEROUS_CONSTRUCTS = [
147
+ /(?:\A|[&|;])\s*(?:start|call|goto)\b/i, # launch / subroutine / jump builtins
148
+ /\b(?:del|erase|rmdir)\b\s+.*[a-z]:\\/i, # drive-letter file removal
149
+ />\s*[a-z]:\\/i # output redirect to a drive-letter path (overwrite)
150
+ ].freeze
151
+ private_constant :CMD_DANGEROUS_CONSTRUCTS
152
+
153
+ def verify_cmd_specific_safe!(cmd)
154
+ return unless Rubik::Platform.windows?
155
+ return unless CMD_DANGEROUS_CONSTRUCTS.any? { |p| cmd.to_s.match?(p) }
156
+
157
+ raise_dangerous(cmd.to_s, rule_name: "cmd.exe_construct")
158
+ end
159
+
160
+ # Security guard gating cmd.exe spawns. The Runner injects the shared
161
+ # session guard via driver_opts (options[:guard]) so operator policy —
162
+ # the live command allowlist above all — applies here too; the lazy
163
+ # local fallback (silent audit, zero policy) serves only standalone /
164
+ # test-constructed drivers.
165
+ def guard
166
+ @options[:guard] || (@guard ||= Rubik::Security::Guard.new(audit_log_path: nil))
167
+ end
168
+
169
+ # Re-run the security guard's classification, scoped to the given shell
170
+ # so cmd.exe semantics are enforced when spawned via cmd. Blocks a
171
+ # POSIX-safe-but-cmd-dangerous command with BlockedWriteError.
172
+ def verify_command_safety!(cmd, shell: nil)
173
+ guard.verify_command_safety!(cmd, shell: shell)
174
+ true
175
+ end
176
+
177
+ # Raise the standard BlockedWriteError used throughout the guard
178
+ # surface for an explicitly detected dangerous construct.
179
+ def raise_dangerous(cmd, rule_name:)
180
+ raise Rubik::Security::BlockedWriteError.new(
181
+ "Dangerous command execution blocked: #{cmd}",
182
+ kind: :cmd_construct,
183
+ blocked_command: cmd,
184
+ rule_name: rule_name
185
+ )
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "driver/base"
5
+ require_relative "driver/process"
6
+ require_relative "driver/docker"
7
+ require_relative "driver/podman"
8
+ require_relative "driver/bubblewrap"
9
+
10
+ module Rubik
11
+ module Sandbox
12
+ module Driver
13
+ @drivers = {}
14
+ @mutex = Monitor.new
15
+
16
+ def self.register(name, driver_class, ctx: nil)
17
+ key = name.to_s.downcase
18
+ restore = @mutex.synchronize do
19
+ prior = @drivers[key] # snapshot BEFORE the overwrite
20
+ @drivers[key] = driver_class
21
+ lambda do
22
+ # Revertible-by-construction: restore only if the slot still
23
+ # holds OUR class — a newer registration (re-registration,
24
+ # another plugin) owns the slot now and must survive this
25
+ # context's disposal. Disposing an overlay must not punch a
26
+ # hole through the built-in underneath it (AGENTS.md sound
27
+ # rollback).
28
+ @mutex.synchronize do
29
+ next unless @drivers.key?(key) && @drivers[key].equal?(driver_class)
30
+
31
+ prior ? @drivers[key] = prior : @drivers.delete(key)
32
+ end
33
+ end
34
+ end
35
+ ctx&.on_dispose(&restore)
36
+ driver_class
37
+ end
38
+
39
+ def self.unregister(name)
40
+ key = name.to_s.downcase
41
+ @mutex.synchronize { @drivers.delete(key) }
42
+ end
43
+
44
+ def self.registered_drivers
45
+ @mutex.synchronize { @drivers.dup }
46
+ end
47
+
48
+ def self.resolve(name, options = {})
49
+ return nil if name.nil?
50
+
51
+ key = name.to_s.downcase
52
+ return nil if ["process", "none"].include?(key)
53
+
54
+ driver_class = @mutex.synchronize { @drivers[key] }
55
+ return nil unless driver_class
56
+
57
+ driver_class.new(options)
58
+ end
59
+
60
+ def self.best_available(preferred = "auto", options = {})
61
+ pref = preferred.to_s.downcase
62
+ if pref != "auto" && !pref.empty?
63
+ return nil if ["process", "none"].include?(pref)
64
+
65
+ candidate = resolve(pref, options)
66
+ return candidate if candidate&.available?
67
+
68
+ return nil
69
+ end
70
+
71
+ if Rubik::Platform.linux?
72
+ bwrap = resolve("bwrap", options)
73
+ return bwrap if bwrap&.available?
74
+ end
75
+
76
+ docker = resolve("docker", options)
77
+ return docker if docker&.available?
78
+
79
+ podman = resolve("podman", options)
80
+ return podman if podman&.available?
81
+
82
+ nil
83
+ end
84
+
85
+ register("bwrap", BubblewrapDriver)
86
+ register("bubblewrap", BubblewrapDriver)
87
+ register("docker", DockerDriver)
88
+ register("podman", PodmanDriver)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "driver"
4
+ require_relative "driver/process"
5
+ require_relative "../security/errors"
6
+ require_relative "../platform"
7
+
8
+ module Rubik
9
+ module Sandbox
10
+ class Runner
11
+ attr_reader :root_dir, :driver, :allowed_external_dirs, :config, :guard
12
+
13
+ def initialize(root_dir: Dir.pwd, allow_network: false, driver: nil, config: nil, allowed_external_dirs: [], guard: nil)
14
+ @root_dir = File.expand_path(root_dir)
15
+ @config = config
16
+ @preferred_driver = driver
17
+ # Shared session guard (Composition provides it). Injected into every
18
+ # driver_opts rebuild below so reconfigure! cycles never strand a
19
+ # policy-less driver — the ProcessDriver consults it on cmd.exe
20
+ # re-verification, where an empty-allowlist guard would silently
21
+ # re-block commands the operator allowlisted.
22
+ @guard = guard
23
+ @allow_network = allow_network || @config&.fetch("sandbox.allow_network", false) || false
24
+ @allowed_external_dirs = (
25
+ Array(allowed_external_dirs) +
26
+ Array(@config&.fetch("workspace.allowed_external_dirs", []))
27
+ ).map { |d| File.expand_path(d.to_s, @root_dir) }.uniq
28
+ # Baseline captured BEFORE the first reconfigure: entries from
29
+ # construction arguments plus the config's current value. Dirs added
30
+ # later via add_allowed_dir (runtime-only) survive reconfigurations.
31
+ @baseline_allowed_dirs = @allowed_external_dirs.dup
32
+
33
+ reconfigure!
34
+
35
+ return unless @config.respond_to?(:on_change)
36
+
37
+ @config.on_change("sandbox") { reconfigure! }
38
+ # Track explicit edits to the allow-list key itself. Once the user
39
+ # has set this key at any scope, the config becomes authoritative:
40
+ # reconfigure! recomputes config-sourced entries from scratch so
41
+ # REMOVING a dir actually shrinks the live allow-list (the previous
42
+ # union-based merge was monotonic and left stale dirs allowed).
43
+ @config.on_change("workspace.allowed_external_dirs") do
44
+ @allowed_dirs_config_touched = true
45
+ reconfigure!
46
+ end
47
+ end
48
+
49
+ def sandbox_enabled?
50
+ return true if @preferred_driver && !@preferred_driver.is_a?(String) && !@preferred_driver.is_a?(Symbol)
51
+ return false if @config&.fetch("sandbox.driver", nil).to_s.downcase.then { |d| ["process", "none"].include?(d) }
52
+ return false if @preferred_driver.to_s.downcase.then { |d| ["process", "none"].include?(d) }
53
+
54
+ val = @config&.fetch("sandbox.enabled", false)
55
+ val == true || val.to_s.downcase == "true"
56
+ end
57
+
58
+ def reconfigure!
59
+ @allow_network = @config.fetch("sandbox.allow_network", @allow_network) if @config
60
+ if @config && @allowed_dirs_config_touched
61
+ # Config authoritative (user has edited the key at some scope):
62
+ # recompute config-sourced entries from scratch so REMOVING a dir
63
+ # shrinks the live allow-list. Runtime-only additions made through
64
+ # add_allowed_dir (absent from the baseline and the config) are
65
+ # preserved; explicit remove_allowed_dir deletions stay deleted
66
+ # because they are absent from both inputs.
67
+ #
68
+ # A malformed (non-Array) value must fail CLOSED: treating it as an
69
+ # empty config list and warning, rather than silently keeping the
70
+ # previously-merged union (which would leave stale dirs allowed).
71
+ raw_allowed = @config.fetch("workspace.allowed_external_dirs", [])
72
+ unless raw_allowed.is_a?(Array)
73
+ warn "[SANDBOX] workspace.allowed_external_dirs must be an Array (got #{raw_allowed.class}); " \
74
+ "treating as empty — the config is authoritative, so stale dirs are revoked."
75
+ raw_allowed = []
76
+ end
77
+ baseline = @baseline_allowed_dirs || []
78
+ config_allowed = raw_allowed.map { |d| File.expand_path(d.to_s, @root_dir) }
79
+ runtime_added = (@allowed_external_dirs - baseline) - config_allowed
80
+ @allowed_external_dirs = (config_allowed + runtime_added).uniq
81
+ end
82
+ # Key untouched: construction baseline + runtime mutations stand —
83
+ # an unrelated sandbox key (enabled, timeout…, 15) must not wipe dirs.
84
+
85
+ @sandbox_enabled = sandbox_enabled?
86
+
87
+ driver_opts = {
88
+ image: @config&.fetch("sandbox.image", "rubik-sandbox:latest"),
89
+ shell: @config&.fetch("sandbox.shell", "auto") || "auto",
90
+ allowed_dirs: allowed_paths,
91
+ guard: @guard
92
+ }
93
+
94
+ @host_driver = Driver::ProcessDriver.new(driver_opts)
95
+
96
+ @driver = if @preferred_driver && !@preferred_driver.is_a?(String) && !@preferred_driver.is_a?(Symbol)
97
+ @preferred_driver
98
+ elsif @sandbox_enabled
99
+ driver_to_use = @preferred_driver || @config&.fetch("sandbox.driver", nil) || "auto"
100
+ Driver.best_available(driver_to_use, driver_opts)
101
+ end
102
+ end
103
+
104
+ def allowed_paths
105
+ roots = [@root_dir]
106
+ if @config
107
+ extra_roots = Array(@config.fetch("workspace.additional_roots", [])).map { |r| File.expand_path(r.to_s, @root_dir) }
108
+ roots.concat(extra_roots)
109
+ end
110
+ roots.concat(@allowed_external_dirs)
111
+ roots.uniq
112
+ end
113
+
114
+ def add_allowed_dir(path)
115
+ expanded = File.expand_path(path.to_s, @root_dir)
116
+ @allowed_external_dirs << expanded unless @allowed_external_dirs.include?(expanded)
117
+ expanded
118
+ end
119
+
120
+ def remove_allowed_dir(path)
121
+ expanded = File.expand_path(path.to_s, @root_dir)
122
+ @allowed_external_dirs.delete(expanded)
123
+ expanded
124
+ end
125
+
126
+ def verify_chdir_safety!(chdir)
127
+ expanded = File.expand_path(chdir.to_s, @root_dir)
128
+ safe = allowed_paths.any? do |allowed|
129
+ Rubik::Platform.path_under?(expanded, allowed)
130
+ end
131
+
132
+ unless safe
133
+ raise Rubik::Security::BlockedWriteError.new(
134
+ "Execution blocked: chdir directory '#{chdir}' (#{expanded}) is outside of allowed workspace roots and external dirs.",
135
+ kind: :chdir_outside_root,
136
+ path: expanded
137
+ )
138
+ end
139
+
140
+ true
141
+ end
142
+
143
+ def allow_network?
144
+ if @config
145
+ cfg_val = @config.fetch("sandbox.allow_network", nil)
146
+ return true if cfg_val == true || cfg_val.to_s.downcase == "true"
147
+ return false if cfg_val == false || cfg_val.to_s.downcase == "false"
148
+
149
+ unless cfg_val.nil?
150
+ warn "[SANDBOX] Unparseable sandbox.allow_network value '#{cfg_val}'; defaulting to fail-closed (false)"
151
+ return false
152
+ end
153
+ end
154
+ @allow_network == true || @allow_network.to_s.downcase == "true"
155
+ end
156
+
157
+ def allow_network
158
+ allow_network?
159
+ end
160
+
161
+ def run(cmd, timeout: nil, chdir: @root_dir)
162
+ chdir_path = File.expand_path(chdir.to_s, @root_dir)
163
+ verify_chdir_safety!(chdir_path)
164
+
165
+ parsed_timeout = timeout&.to_i
166
+ effective_timeout = (parsed_timeout&.positive? ? parsed_timeout : nil) ||
167
+ (@config && @config.fetch("sandbox.timeout_seconds", 15).to_i) || 15
168
+ effective_timeout = 15 if effective_timeout <= 0
169
+ mem_limit = @config&.fetch("sandbox.memory_limit_mb", 512) || 512
170
+ cpus = @config&.fetch("sandbox.cpus", "1.0") || "1.0"
171
+
172
+ active_driver = if sandbox_enabled? && @driver
173
+ @driver
174
+ else
175
+ @host_driver || Driver::ProcessDriver.new(shell: @config&.fetch("sandbox.shell", "auto") || "auto",
176
+ guard: @guard)
177
+ end
178
+
179
+ res = active_driver.run(
180
+ cmd,
181
+ timeout: effective_timeout,
182
+ chdir: chdir_path,
183
+ allow_network: allow_network?,
184
+ memory_limit_mb: mem_limit,
185
+ cpus: cpus
186
+ )
187
+
188
+ if res.is_a?(Hash)
189
+ res = res.dup if res.frozen?
190
+ # Session review B7: the per-run isolation line confused more than
191
+ # it informed once operators settled their setup — `sandbox.isolation_note`
192
+ # (default true, the historical behavior) can silence it.
193
+ res[:isolation] = isolation_disclosure if disclose_isolation?
194
+ end
195
+ res
196
+ end
197
+
198
+ def disclose_isolation?
199
+ @config.respond_to?(:fetch) ? @config.fetch("sandbox.isolation_note", true) != false : true
200
+ end
201
+
202
+ def isolation_disclosure
203
+ if sandbox_enabled? && @driver
204
+ net_status = allow_network? ? "allowed" : "none"
205
+ "🔍 Isolation: sandbox (#{@driver.name}, network=#{net_status}) — strong"
206
+ else
207
+ "🔍 Execution: direct (safe execution guardrails active; sandbox disabled)"
208
+ end
209
+ end
210
+
211
+ def isolation_level
212
+ return "sandbox (#{@driver.name})" if sandbox_enabled? && @driver
213
+
214
+ "direct (safe host)"
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+
5
+ module Rubik
6
+ module Sandbox
7
+ # Decides which shell executes a run_command string and how (Plan 55).
8
+ # Single source of truth for shell identity: the process driver spawns
9
+ # through it, and the system prompt's <environment> line is generated
10
+ # from it. Pure functions only — no global state, no process spawns.
11
+ #
12
+ # Auto-routing is syntax-gated, drifting toward bash on doubt: a
13
+ # false positive (plain command under bash) is harmless because bash is
14
+ # a superset of cmd for our purposes, while a false negative (POSIX
15
+ # syntax under cmd) silently corrupts results.
16
+ module ShellResolver
17
+ SHELLS = ["auto", "bash", "pwsh", "cmd", "sh"].freeze
18
+ POWERSHELL = "pwsh"
19
+
20
+ # Syntax cmd.exe (and pwsh) cannot execute correctly.
21
+ BASH_REQUIRED = %r{<\(|>\(|/dev/null|<<[-~]?["']?[A-Za-z]|<<<}
22
+ # PowerShell-idiomatic tokens: $env: plus any Verb-Noun cmdlet shape
23
+ # (Select-String, Sort-Object, ConvertTo-Json, Out-File, ...). Live
24
+ # feedback (Plan 55 §9): models reach for Select-String as readily as
25
+ # Get-*, so the shape must be generic, not a Get-* prefix.
26
+ PS_FINGERPRINT = /\$env:|\b[A-Z][a-z]+(?:[A-Z][a-z]+)*-[A-Z][A-Za-z]+\b/
27
+ # Shell substitution/variables cmd.exe silently mangles. Paired single
28
+ # quotes are POSIX quoting: cmd.exe passes them through literally,
29
+ # which breaks tools like `gh --jq '...'` (Plan 55 §9, live feedback).
30
+ SUBSTITUTION = /\$\(|`|\$[A-Za-z_]|'[^']*'/
31
+
32
+ # Windows ships a WSL launcher as System32\bash.exe. It must never be
33
+ # auto-selected for repo commands: without a distro it just errors,
34
+ # and with one it would run Linux git against a Linux filesystem view.
35
+ WSL_STUB = %r{/system32/bash\.exe$}i
36
+ # Session review B7: bare `dir`/`where` are cmd.exe builtins with NO
37
+ # binary — under a bash-resolved spawn (POSIX quoting elsewhere in the
38
+ # command pulled it to bash) they died with ENOENT and the diagnostic
39
+ # engine filed them as MISSING_BINARY. Non-destructive listing/
40
+ # display builtins only: assoc/ftype mutate file-association state and
41
+ # the destructive ones (del, rd, start, call, ...) stay on their
42
+ # existing guard-verified paths untouched.
43
+ CMD_BUILTINS = ["dir", "where", "cls", "ver", "vol", "title", "prompt"].freeze
44
+ GIT_BASH_CANDIDATES = [
45
+ "C:/Program Files/Git/bin/bash.exe",
46
+ "C:/Program Files/Git/usr/bin/bash.exe",
47
+ "C:/Program Files (x86)/Git/bin/bash.exe"
48
+ ].freeze
49
+
50
+ module_function
51
+
52
+ # Returns { argv:, shell:, note: }. argv is spawned verbatim via
53
+ # Open3's array form; note (rare) explains a fallback and is meant to
54
+ # be prepended to the command's stderr. On Windows a resolved bash is
55
+ # always spawned by full path so PATH can never substitute the WSL
56
+ # launcher between resolution and spawn.
57
+ def resolve(cmd, platform: Rubik::Platform.os_family, preference: "auto", git_bash_candidates: GIT_BASH_CANDIDATES)
58
+ pref = normalize(preference)
59
+ chosen = pref == "auto" ? gate(cmd, platform) : pref
60
+
61
+ note = nil
62
+ unless available?(chosen, git_bash_candidates, platform: platform)
63
+ note = "Shell '#{chosen}' not found; falling back to #{default_shell(platform)}"
64
+ note += " (shell-specific syntax in this command may fail)" if chosen != default_shell(platform)
65
+ chosen = default_shell(platform)
66
+ end
67
+
68
+ argv = argv_for(chosen, cmd)
69
+ argv = [bash_path(git_bash_candidates, platform: platform), "-c", cmd] if chosen == "bash" && platform == :windows
70
+
71
+ { argv: argv, shell: chosen, note: note }
72
+ end
73
+
74
+ def default_shell(platform)
75
+ return "cmd" if platform == :windows
76
+
77
+ available?("bash", platform: platform) ? "bash" : "sh"
78
+ end
79
+
80
+ # One-line machine-readable shell facts for the system prompt.
81
+ def environment_line(preference = "auto", platform: Rubik::Platform.os_family)
82
+ pref = normalize(preference)
83
+ shell_desc = if pref != "auto"
84
+ pref
85
+ elsif platform == :windows
86
+ "cmd (plain commands) | bash (POSIX quoting/syntax: '...', <(...), $(...), /dev/null) | " \
87
+ "pwsh (PowerShell cmdlets) — auto-routed per command; escape hatch: bash -c '...'"
88
+ else
89
+ default_shell(platform)
90
+ end
91
+ %(<environment os="#{platform}" shell="#{shell_desc}" />)
92
+ end
93
+
94
+ def argv_for(shell, cmd)
95
+ case shell
96
+ when "bash", "sh" then [shell, "-c", cmd]
97
+ when POWERSHELL then [POWERSHELL, "-NoProfile", "-NonInteractive", "-Command", cmd]
98
+ when "cmd" then ["cmd", "/c", cmd]
99
+ else [cmd]
100
+ end
101
+ end
102
+
103
+ def available?(shell, git_bash_candidates = GIT_BASH_CANDIDATES, platform: Rubik::Platform.os_family)
104
+ if platform == :windows
105
+ return !bash_path(git_bash_candidates, platform: platform).nil? if shell == "bash"
106
+ return true if shell == "cmd"
107
+
108
+ if shell == "pwsh"
109
+ pwsh_found = Rubik::Platform.which("pwsh") || Rubik::Platform.which("powershell")
110
+ return true if pwsh_found
111
+ return false if Rubik::Platform.windows?
112
+
113
+ # Simulated :windows platform on non-windows test hosts:
114
+ # If bash/cmd are stubbed to Windows paths but pwsh is nil, respect the missing stub
115
+ b_hit = Rubik::Platform.which("bash")
116
+ c_hit = Rubik::Platform.which("cmd")
117
+ win_stubbed = (b_hit && b_hit =~ %r{\A[a-zA-Z]:/}) || (c_hit && c_hit =~ %r{\A[a-zA-Z]:/})
118
+ return false if win_stubbed
119
+
120
+ return true
121
+ end
122
+ end
123
+
124
+ !Rubik::Platform.which(shell).nil?
125
+ end
126
+
127
+ # Usable bash on Windows: the PATH hit unless it is the WSL launcher,
128
+ # else bash derived from the installed git.exe (covers non-standard
129
+ # install roots like C:/Git or scoop), else the static Git for
130
+ # Windows paths. Nil means "no usable bash".
131
+ def bash_path(git_candidates = GIT_BASH_CANDIDATES, platform: Rubik::Platform.os_family)
132
+ candidate = Rubik::Platform.which("bash")
133
+ return candidate if candidate && !candidate.match?(WSL_STUB) && (platform != :windows || candidate.end_with?(".exe") || candidate.include?("/"))
134
+
135
+ git_exe = Rubik::Platform.which("git")
136
+ if git_exe
137
+ root = git_install_root(git_exe)
138
+ ["#{root}/bin/bash.exe", "#{root}/usr/bin/bash.exe", "#{root}/mingw64/bin/bash.exe"].each do |p|
139
+ return p if File.file?(p)
140
+ end
141
+ end
142
+ git_candidates.find { |p| File.file?(p) }
143
+ end
144
+
145
+ # C:/Git/cmd/git.exe -> C:/Git; C:/Git/mingw64/bin/git.exe -> C:/Git
146
+ # (lazy match + mingw64/bin first: bash lives at the INSTALL root's
147
+ # bin|usr/bin, not under mingw64)
148
+ def git_install_root(git_exe)
149
+ m = git_exe.match(%r{\A(.+?)/(?:mingw64/bin|usr/bin|cmd|bin)/git\.exe\z}i)
150
+ m ? m[1] : File.dirname(git_exe)
151
+ end
152
+
153
+ def normalize(preference)
154
+ pref = preference.to_s.downcase
155
+ SHELLS.include?(pref) ? pref : "auto"
156
+ end
157
+
158
+ def gate(cmd, platform)
159
+ # BASH_REQUIRED outranks the builtin route (review B7): `where git
160
+ # 2>/dev/null` must stay an honest bash run — cmd would mangle the
161
+ # POSIX redirect into a bogus drive path. CMD_BUILTINS still precede
162
+ # SUBSTITUTION so plain `dir 'some filter'` quoting reaches cmd.
163
+ return "bash" if BASH_REQUIRED.match?(cmd)
164
+ return "cmd" if platform == :windows && cmd_builtin?(cmd)
165
+ return POWERSHELL if PS_FINGERPRINT.match?(cmd)
166
+ return "bash" if platform == :windows && SUBSTITUTION.match?(cmd)
167
+
168
+ default_shell(platform)
169
+ end
170
+
171
+ # First token is a cmd.exe builtin with no standalone binary — cmd.exe
172
+ # is the only interpreter that can run it (see CMD_BUILTINS rationale).
173
+ def cmd_builtin?(cmd)
174
+ CMD_BUILTINS.include?(cmd.to_s.split.first.to_s.downcase)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sandbox/runner"
4
+
5
+ module Rubik
6
+ module Sandbox
7
+ end
8
+ end