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.
- checksums.yaml +7 -0
- data/ATTRIBUTION.md +139 -0
- data/CHANGELOG.md +236 -0
- data/LICENSE +21 -0
- data/README.md +269 -0
- data/bin/rubik +82 -0
- data/lib/rubik/adapter/base.rb +195 -0
- data/lib/rubik/adapter/c.rb +36 -0
- data/lib/rubik/adapter/crystal.rb +78 -0
- data/lib/rubik/adapter/generic.rb +86 -0
- data/lib/rubik/adapter/go.rb +36 -0
- data/lib/rubik/adapter/node.rb +146 -0
- data/lib/rubik/adapter/process.rb +78 -0
- data/lib/rubik/adapter/python.rb +30 -0
- data/lib/rubik/adapter/registry.rb +81 -0
- data/lib/rubik/adapter/ruby.rb +30 -0
- data/lib/rubik/adapter/rust.rb +36 -0
- data/lib/rubik/adapter/zig.rb +36 -0
- data/lib/rubik/adapter.rb +18 -0
- data/lib/rubik/ast/body_loader.rb +19 -0
- data/lib/rubik/ast/c_extractor.rb +114 -0
- data/lib/rubik/ast/cache_store.rb +191 -0
- data/lib/rubik/ast/extractor.rb +59 -0
- data/lib/rubik/ast/fallback_cleaner.rb +145 -0
- data/lib/rubik/ast/go_extractor.rb +148 -0
- data/lib/rubik/ast/graph.rb +272 -0
- data/lib/rubik/ast/python_extractor.rb +120 -0
- data/lib/rubik/ast/ruby_extractor.rb +260 -0
- data/lib/rubik/ast/rust_extractor.rb +127 -0
- data/lib/rubik/ast/symbol.rb +103 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
- data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
- data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
- data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
- data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
- data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
- data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
- data/lib/rubik/ast/treesitter_extractor.rb +93 -0
- data/lib/rubik/ast/typescript_extractor.rb +283 -0
- data/lib/rubik/ast/zig_extractor.rb +116 -0
- data/lib/rubik/ast.rb +13 -0
- data/lib/rubik/benchmark/runner.rb +224 -0
- data/lib/rubik/benchmark/synth_repo.rb +59 -0
- data/lib/rubik/benchmark.rb +9 -0
- data/lib/rubik/buffer/diff.rb +43 -0
- data/lib/rubik/buffer/manager.rb +427 -0
- data/lib/rubik/buffer/path_validator.rb +60 -0
- data/lib/rubik/buffer/snapshot.rb +99 -0
- data/lib/rubik/buffer.rb +10 -0
- data/lib/rubik/coeffects.rb +129 -0
- data/lib/rubik/concurrency/boundary.rb +74 -0
- data/lib/rubik/concurrency/dns_pool.rb +68 -0
- data/lib/rubik/concurrency/errors.rb +17 -0
- data/lib/rubik/concurrency/event_loop.rb +441 -0
- data/lib/rubik/concurrency/join.rb +55 -0
- data/lib/rubik/concurrency/process_waiter.rb +55 -0
- data/lib/rubik/concurrency/scheduler.rb +84 -0
- data/lib/rubik/concurrency/timers.rb +109 -0
- data/lib/rubik/concurrency.rb +132 -0
- data/lib/rubik/config/deep_ops.rb +80 -0
- data/lib/rubik/config/defaults.rb +248 -0
- data/lib/rubik/config/model_catalog.rb +93 -0
- data/lib/rubik/config/store.rb +155 -0
- data/lib/rubik/config.rb +216 -0
- data/lib/rubik/context/git_info.rb +84 -0
- data/lib/rubik/context/git_lister.rb +37 -0
- data/lib/rubik/context/index.rb +134 -0
- data/lib/rubik/context/repo_context_reader.rb +121 -0
- data/lib/rubik/context/scanner.rb +439 -0
- data/lib/rubik/context.rb +11 -0
- data/lib/rubik/diagnostics/collector.rb +200 -0
- data/lib/rubik/diagnostics/entry.rb +60 -0
- data/lib/rubik/diagnostics/parser.rb +107 -0
- data/lib/rubik/diagnostics.rb +10 -0
- data/lib/rubik/event_bus.rb +56 -0
- data/lib/rubik/kernel/dispatcher.rb +171 -0
- data/lib/rubik/kernel/dmesg.rb +116 -0
- data/lib/rubik/kernel/init.rb +177 -0
- data/lib/rubik/kernel/record.rb +85 -0
- data/lib/rubik/kernel/unit.rb +160 -0
- data/lib/rubik/llm/anthropic.rb +202 -0
- data/lib/rubik/llm/chunk_assembler.rb +285 -0
- data/lib/rubik/llm/client.rb +526 -0
- data/lib/rubik/llm/formatter.rb +450 -0
- data/lib/rubik/llm/message_sanitizer.rb +87 -0
- data/lib/rubik/llm/ollama.rb +167 -0
- data/lib/rubik/llm/openai.rb +21 -0
- data/lib/rubik/llm/openai_compatible.rb +216 -0
- data/lib/rubik/llm/provider.rb +125 -0
- data/lib/rubik/llm/registry.rb +336 -0
- data/lib/rubik/llm/router.rb +100 -0
- data/lib/rubik/llm.rb +18 -0
- data/lib/rubik/log.rb +38 -0
- data/lib/rubik/lsp/client.rb +475 -0
- data/lib/rubik/lsp/manager.rb +366 -0
- data/lib/rubik/lsp.rb +9 -0
- data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
- data/lib/rubik/mcp/client.rb +169 -0
- data/lib/rubik/mcp/errors.rb +11 -0
- data/lib/rubik/mcp/manager.rb +619 -0
- data/lib/rubik/mcp/proxy_tool.rb +104 -0
- data/lib/rubik/mcp/transport/stdio.rb +255 -0
- data/lib/rubik/mcp.rb +12 -0
- data/lib/rubik/orchestrator/activity_log.rb +75 -0
- data/lib/rubik/orchestrator/auto_apply.rb +166 -0
- data/lib/rubik/orchestrator/budget_manager.rb +97 -0
- data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
- data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
- data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
- data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
- data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
- data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
- data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
- data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
- data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
- data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
- data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
- data/lib/rubik/orchestrator/goal_state.rb +184 -0
- data/lib/rubik/orchestrator/goal_wait.rb +125 -0
- data/lib/rubik/orchestrator/loop.rb +388 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
- data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
- data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
- data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
- data/lib/rubik/orchestrator/shell_deps.rb +85 -0
- data/lib/rubik/orchestrator/step_executor.rb +328 -0
- data/lib/rubik/orchestrator/stream_manager.rb +118 -0
- data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
- data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
- data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
- data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
- data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
- data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
- data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
- data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
- data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
- data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
- data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
- data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
- data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
- data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
- data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
- data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
- data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
- data/lib/rubik/orchestrator/tool_history.rb +39 -0
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
- data/lib/rubik/orchestrator/turn_executor.rb +376 -0
- data/lib/rubik/orchestrator.rb +11 -0
- data/lib/rubik/platform/filesystem.rb +64 -0
- data/lib/rubik/platform/io_drain.rb +53 -0
- data/lib/rubik/platform/path.rb +210 -0
- data/lib/rubik/platform/process.rb +38 -0
- data/lib/rubik/platform/sanitize.rb +157 -0
- data/lib/rubik/platform.rb +113 -0
- data/lib/rubik/plugin/dsl.rb +51 -0
- data/lib/rubik/plugin/manager.rb +247 -0
- data/lib/rubik/plugin/storage.rb +195 -0
- data/lib/rubik/plugin.rb +10 -0
- data/lib/rubik/prompt/context_builder.rb +377 -0
- data/lib/rubik/prompt/context_compactor.rb +326 -0
- data/lib/rubik/prompt/conversation_rows.rb +74 -0
- data/lib/rubik/prompt/doctrine.rb +160 -0
- data/lib/rubik/prompt/emergency_compactor.rb +81 -0
- data/lib/rubik/prompt/services.rb +60 -0
- data/lib/rubik/prompt/token_estimator.rb +63 -0
- data/lib/rubik/prompt/tool_teachings.rb +78 -0
- data/lib/rubik/prompt.rb +13 -0
- data/lib/rubik/repair/loop.rb +95 -0
- data/lib/rubik/repair.rb +8 -0
- data/lib/rubik/repl/ast_warmup.rb +129 -0
- data/lib/rubik/repl/command_context.rb +214 -0
- data/lib/rubik/repl/command_parser.rb +48 -0
- data/lib/rubik/repl/commands/agent.rb +30 -0
- data/lib/rubik/repl/commands/agent_context.rb +289 -0
- data/lib/rubik/repl/commands/agent_execution.rb +342 -0
- data/lib/rubik/repl/commands/agent_models.rb +212 -0
- data/lib/rubik/repl/commands/agent_providers.rb +232 -0
- data/lib/rubik/repl/commands/base.rb +55 -0
- data/lib/rubik/repl/commands/buffer.rb +124 -0
- data/lib/rubik/repl/commands/dmesg.rb +87 -0
- data/lib/rubik/repl/commands/lsp.rb +235 -0
- data/lib/rubik/repl/commands/mcp.rb +194 -0
- data/lib/rubik/repl/commands/plugins.rb +208 -0
- data/lib/rubik/repl/commands/pruner.rb +71 -0
- data/lib/rubik/repl/commands/session.rb +183 -0
- data/lib/rubik/repl/commands/skills.rb +111 -0
- data/lib/rubik/repl/commands/storage.rb +85 -0
- data/lib/rubik/repl/commands/subagents.rb +195 -0
- data/lib/rubik/repl/commands/system.rb +85 -0
- data/lib/rubik/repl/commands/tasks.rb +120 -0
- data/lib/rubik/repl/commands/workspace.rb +443 -0
- data/lib/rubik/repl/commands.rb +44 -0
- data/lib/rubik/repl/composition.rb +32 -0
- data/lib/rubik/repl/config_wiring.rb +142 -0
- data/lib/rubik/repl/confirmations.rb +56 -0
- data/lib/rubik/repl/hooks.rb +47 -0
- data/lib/rubik/repl/input_execution.rb +323 -0
- data/lib/rubik/repl/input_loop.rb +213 -0
- data/lib/rubik/repl/interrupt_handler.rb +217 -0
- data/lib/rubik/repl/lifecycle.rb +410 -0
- data/lib/rubik/repl/paste_support.rb +88 -0
- data/lib/rubik/repl/pending_notice_queue.rb +60 -0
- data/lib/rubik/repl/presentation.rb +132 -0
- data/lib/rubik/repl/pruner/brief.rb +101 -0
- data/lib/rubik/repl/pruner/gate.rb +279 -0
- data/lib/rubik/repl/pruner/harness.rb +236 -0
- data/lib/rubik/repl/pruner/rules.rb +316 -0
- data/lib/rubik/repl/pruner/service.rb +82 -0
- data/lib/rubik/repl/pruner.rb +39 -0
- data/lib/rubik/repl/service_catalog.rb +374 -0
- data/lib/rubik/repl/setup_factories.rb +66 -0
- data/lib/rubik/repl/shell.rb +231 -0
- data/lib/rubik/repl/step_formatter.rb +324 -0
- data/lib/rubik/repl/tee_io.rb +105 -0
- data/lib/rubik/repl/terminal_guard.rb +157 -0
- data/lib/rubik/repl/turn_recorder.rb +234 -0
- data/lib/rubik/repl.rb +14 -0
- data/lib/rubik/runtime/ast_service.rb +78 -0
- data/lib/rubik/runtime/context.rb +335 -0
- data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
- data/lib/rubik/runtime/discovery.rb +342 -0
- data/lib/rubik/runtime/fiber.rb +142 -0
- data/lib/rubik/runtime/fiber_ordering.rb +52 -0
- data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
- data/lib/rubik/runtime/index_service.rb +58 -0
- data/lib/rubik/runtime/loop_service.rb +73 -0
- data/lib/rubik/runtime/manager.rb +241 -0
- data/lib/rubik/runtime/overlay_storage.rb +481 -0
- data/lib/rubik/runtime/service_definition.rb +54 -0
- data/lib/rubik/runtime/service_registration.rb +33 -0
- data/lib/rubik/runtime/task_manager.rb +302 -0
- data/lib/rubik/runtime.rb +19 -0
- data/lib/rubik/sandbox/driver/base.rb +30 -0
- data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
- data/lib/rubik/sandbox/driver/docker.rb +113 -0
- data/lib/rubik/sandbox/driver/podman.rb +98 -0
- data/lib/rubik/sandbox/driver/process.rb +190 -0
- data/lib/rubik/sandbox/driver.rb +91 -0
- data/lib/rubik/sandbox/runner.rb +218 -0
- data/lib/rubik/sandbox/shell_resolver.rb +178 -0
- data/lib/rubik/sandbox.rb +8 -0
- data/lib/rubik/security/blocked_guidance.rb +187 -0
- data/lib/rubik/security/classifier/classifier.rb +440 -0
- data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
- data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
- data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
- data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
- data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
- data/lib/rubik/security/classifier/commands/black.yaml +12 -0
- data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
- data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
- data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
- data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
- data/lib/rubik/security/classifier/commands/column.yaml +7 -0
- data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
- data/lib/rubik/security/classifier/commands/command.yaml +12 -0
- data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
- data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
- data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
- data/lib/rubik/security/classifier/commands/date.yaml +10 -0
- data/lib/rubik/security/classifier/commands/df.yaml +7 -0
- data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
- data/lib/rubik/security/classifier/commands/du.yaml +7 -0
- data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
- data/lib/rubik/security/classifier/commands/env.yaml +15 -0
- data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
- data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
- data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
- data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/false.yaml +7 -0
- data/lib/rubik/security/classifier/commands/file.yaml +7 -0
- data/lib/rubik/security/classifier/commands/find.yaml +25 -0
- data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
- data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
- data/lib/rubik/security/classifier/commands/free.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
- data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
- data/lib/rubik/security/classifier/commands/git.yaml +524 -0
- data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
- data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
- data/lib/rubik/security/classifier/commands/go.yaml +44 -0
- data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
- data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
- data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
- data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
- data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
- data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
- data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
- data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
- data/lib/rubik/security/classifier/commands/head.yaml +7 -0
- data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
- data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/id.yaml +7 -0
- data/lib/rubik/security/classifier/commands/install.yaml +7 -0
- data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
- data/lib/rubik/security/classifier/commands/java.yaml +35 -0
- data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
- data/lib/rubik/security/classifier/commands/join.yaml +7 -0
- data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
- data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
- data/lib/rubik/security/classifier/commands/less.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
- data/lib/rubik/security/classifier/commands/make.yaml +21 -0
- data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
- data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
- data/lib/rubik/security/classifier/commands/more.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
- data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
- data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
- data/lib/rubik/security/classifier/commands/node.yaml +20 -0
- data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
- data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
- data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
- data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
- data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
- data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
- data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
- data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
- data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
- data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
- data/lib/rubik/security/classifier/commands/python.yaml +9 -0
- data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
- data/lib/rubik/security/classifier/commands/read.yaml +7 -0
- data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
- data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
- data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
- data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
- data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
- data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
- data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
- data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
- data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
- data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
- data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
- data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
- data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
- data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
- data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
- data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
- data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
- data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
- data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
- data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
- data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
- data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
- data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
- data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
- data/lib/rubik/security/classifier/commands/test.yaml +7 -0
- data/lib/rubik/security/classifier/commands/time.yaml +8 -0
- data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
- data/lib/rubik/security/classifier/commands/top.yaml +7 -0
- data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
- data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
- data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
- data/lib/rubik/security/classifier/commands/true.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
- data/lib/rubik/security/classifier/commands/type.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
- data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
- data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
- data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
- data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
- data/lib/rubik/security/classifier/commands/which.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
- data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
- data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
- data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
- data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
- data/lib/rubik/security/classifier/database.rb +216 -0
- data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
- data/lib/rubik/security/classifier/matcher/options.rb +245 -0
- data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
- data/lib/rubik/security/classifier/matcher.rb +249 -0
- data/lib/rubik/security/classifier/models.rb +170 -0
- data/lib/rubik/security/classifier/parser.rb +23 -0
- data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
- data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
- data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
- data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
- data/lib/rubik/security/classifier.rb +25 -0
- data/lib/rubik/security/errors.rb +39 -0
- data/lib/rubik/security/guard.rb +954 -0
- data/lib/rubik/security/redactor.rb +89 -0
- data/lib/rubik/security.rb +11 -0
- data/lib/rubik/session/manager.rb +331 -0
- data/lib/rubik/session/recorder.rb +149 -0
- data/lib/rubik/session/replayer.rb +65 -0
- data/lib/rubik/session.rb +10 -0
- data/lib/rubik/skills/frontmatter.rb +41 -0
- data/lib/rubik/skills/manager.rb +223 -0
- data/lib/rubik/skills/registry.rb +64 -0
- data/lib/rubik/skills/skill.rb +33 -0
- data/lib/rubik/skills/storage.rb +77 -0
- data/lib/rubik/skills/validator.rb +116 -0
- data/lib/rubik/skills.rb +20 -0
- data/lib/rubik/storage/folding.rb +23 -0
- data/lib/rubik/storage/layout.rb +145 -0
- data/lib/rubik/storage/render.rb +97 -0
- data/lib/rubik/storage/service.rb +42 -0
- data/lib/rubik/storage/store.rb +514 -0
- data/lib/rubik/storage/transcript_indexer.rb +116 -0
- data/lib/rubik/storage/uri_reader.rb +168 -0
- data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
- data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
- data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
- data/lib/rubik/storage/vakanuvis/service.rb +69 -0
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
- data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
- data/lib/rubik/storage/vakanuvis.rb +77 -0
- data/lib/rubik/storage.rb +29 -0
- data/lib/rubik/tools/base.rb +107 -0
- data/lib/rubik/tools/builtins/dmesg.rb +74 -0
- data/lib/rubik/tools/builtins/exec.rb +293 -0
- data/lib/rubik/tools/builtins/files.rb +354 -0
- data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
- data/lib/rubik/tools/builtins/grep.rb +281 -0
- data/lib/rubik/tools/builtins/interactive.rb +175 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
- data/lib/rubik/tools/builtins/lsp.rb +283 -0
- data/lib/rubik/tools/builtins/pruner.rb +88 -0
- data/lib/rubik/tools/builtins/runtime.rb +120 -0
- data/lib/rubik/tools/builtins/send_message.rb +109 -0
- data/lib/rubik/tools/builtins/show_file.rb +84 -0
- data/lib/rubik/tools/builtins/skill.rb +72 -0
- data/lib/rubik/tools/builtins/storage.rb +232 -0
- data/lib/rubik/tools/builtins/subagent.rb +356 -0
- data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
- data/lib/rubik/tools/builtins/symbols.rb +209 -0
- data/lib/rubik/tools/builtins/synthesis.rb +314 -0
- data/lib/rubik/tools/builtins/task.rb +168 -0
- data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
- data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
- data/lib/rubik/tools/builtins.rb +30 -0
- data/lib/rubik/tools/catalog.rb +70 -0
- data/lib/rubik/tools/declaration.rb +90 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
- data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
- data/lib/rubik/tools/native_search.rb +180 -0
- data/lib/rubik/tools/output.rb +54 -0
- data/lib/rubik/tools/preflight_runner.rb +209 -0
- data/lib/rubik/tools/registry.rb +247 -0
- data/lib/rubik/tools/result.rb +71 -0
- data/lib/rubik/tools/services.rb +96 -0
- data/lib/rubik/tools/truncator.rb +78 -0
- data/lib/rubik/tools/uri_resolver.rb +195 -0
- data/lib/rubik/tools.rb +14 -0
- data/lib/rubik/ui/approvals.rb +52 -0
- data/lib/rubik/ui/banner.rb +43 -0
- data/lib/rubik/ui/capability.rb +122 -0
- data/lib/rubik/ui/dialog.rb +106 -0
- data/lib/rubik/ui/diff_viewer.rb +78 -0
- data/lib/rubik/ui/frame.rb +95 -0
- data/lib/rubik/ui/markdown_stream.rb +107 -0
- data/lib/rubik/ui/panel.rb +98 -0
- data/lib/rubik/ui/renderer.rb +206 -0
- data/lib/rubik/ui/spinner.rb +120 -0
- data/lib/rubik/ui/statusline.rb +134 -0
- data/lib/rubik/ui/theme.rb +118 -0
- data/lib/rubik/ui.rb +57 -0
- data/lib/rubik/uris.rb +101 -0
- data/lib/rubik/version.rb +5 -0
- data/lib/rubik.rb +65 -0
- 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
|