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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tools/base"
|
|
4
|
+
require_relative "../tools/result"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module MCP
|
|
8
|
+
class ProxyTool < Rubik::Tools::BaseTool
|
|
9
|
+
attr_reader :server_name, :original_name, :client
|
|
10
|
+
|
|
11
|
+
def initialize(server_name:, tool_def:, client:)
|
|
12
|
+
@server_name = server_name.to_s
|
|
13
|
+
@client = client
|
|
14
|
+
@original_name = (tool_def["name"] || tool_def[:name]).to_s
|
|
15
|
+
|
|
16
|
+
sanitized_server = @server_name.downcase.gsub(/[^a-z0-9_]/, "_")
|
|
17
|
+
sanitized_tool = @original_name.downcase.gsub(/[^a-z0-9_]/, "_")
|
|
18
|
+
tool_name = "mcp_#{sanitized_server}_#{sanitized_tool}"
|
|
19
|
+
|
|
20
|
+
raw_desc = (tool_def["description"] || tool_def[:description] || "").to_s.strip
|
|
21
|
+
desc = raw_desc.empty? ? "[MCP: #{@server_name}] #{@original_name}" : "[MCP: #{@server_name}] #{raw_desc}"
|
|
22
|
+
raw_schema = tool_def["inputSchema"] || tool_def[:inputSchema]
|
|
23
|
+
schema = raw_schema.is_a?(Hash) ? raw_schema.dup : {}
|
|
24
|
+
schema["type"] = "object" unless schema["type"].is_a?(String)
|
|
25
|
+
schema["properties"] = {} unless schema["properties"].is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
super(
|
|
28
|
+
name: tool_name,
|
|
29
|
+
description: desc,
|
|
30
|
+
risk: risk_from_annotations(tool_def["annotations"] || tool_def[:annotations]),
|
|
31
|
+
params_schema: schema
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Maps MCP tool annotations onto Rubik's risk levels so approval
|
|
36
|
+
# guardrails can distinguish read-only MCP tools from destructive or
|
|
37
|
+
# open-world ones. Unannotated tools stay conservative (:execute).
|
|
38
|
+
def risk_from_annotations(annotations)
|
|
39
|
+
return :execute unless annotations.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
if annotations["readOnlyHint"] == true
|
|
42
|
+
:read
|
|
43
|
+
elsif annotations["destructiveHint"] == true
|
|
44
|
+
:write
|
|
45
|
+
else
|
|
46
|
+
:execute
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def execute(args = {})
|
|
51
|
+
args_hash = args.is_a?(Hash) ? args : {}
|
|
52
|
+
res = @client.call_tool(@original_name, args_hash)
|
|
53
|
+
|
|
54
|
+
is_error = if res.respond_to?(:error?)
|
|
55
|
+
res.error?
|
|
56
|
+
elsif res.is_a?(Hash) || res.respond_to?(:[])
|
|
57
|
+
res["isError"] == true || res[:isError] == true
|
|
58
|
+
else
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
output_text = if res.is_a?(Hash)
|
|
63
|
+
if res["text"]
|
|
64
|
+
res["text"].to_s
|
|
65
|
+
elsif res["content"].is_a?(Array)
|
|
66
|
+
res["content"].select { |c| (c["type"] || c[:type]) == "text" }
|
|
67
|
+
.map { |c| (c["text"] || c[:text]).to_s }
|
|
68
|
+
.join("\n")
|
|
69
|
+
else
|
|
70
|
+
res.inspect
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
res.to_s
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Output size is bounded by the orchestrator's Truncator (single
|
|
77
|
+
# truncation point, head_tail strategy); no pre-truncation here so
|
|
78
|
+
# the strategy stays consistent for MCP and built-in tools alike.
|
|
79
|
+
|
|
80
|
+
if is_error
|
|
81
|
+
Rubik::Tools::Result.new(
|
|
82
|
+
tool_name: @name,
|
|
83
|
+
status: :error,
|
|
84
|
+
output: output_text,
|
|
85
|
+
error: output_text
|
|
86
|
+
)
|
|
87
|
+
else
|
|
88
|
+
Rubik::Tools::Result.new(
|
|
89
|
+
tool_name: @name,
|
|
90
|
+
status: :ok,
|
|
91
|
+
output: output_text
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
Rubik::Tools::Result.new(
|
|
96
|
+
tool_name: @name,
|
|
97
|
+
status: :error,
|
|
98
|
+
output: e.message,
|
|
99
|
+
error: e.message
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require_relative "../errors"
|
|
7
|
+
require_relative "../../platform"
|
|
8
|
+
require_relative "../../security/guard"
|
|
9
|
+
|
|
10
|
+
module Rubik
|
|
11
|
+
module MCP
|
|
12
|
+
module Transport
|
|
13
|
+
class Stdio
|
|
14
|
+
attr_reader :command, :args, :env, :root_dir, :running, :wait_thr
|
|
15
|
+
|
|
16
|
+
def initialize(command, args: [], env: {}, root_dir: Dir.pwd, guard: nil)
|
|
17
|
+
@command = command
|
|
18
|
+
@args = Array(args).map(&:to_s)
|
|
19
|
+
@env = env || {}
|
|
20
|
+
@root_dir = File.expand_path(root_dir)
|
|
21
|
+
@guard = guard || Rubik::Security::Guard.new
|
|
22
|
+
@running = false
|
|
23
|
+
@req_id = 0
|
|
24
|
+
@stdin = nil
|
|
25
|
+
@stdout = nil
|
|
26
|
+
@stderr = nil
|
|
27
|
+
@wait_thr = nil
|
|
28
|
+
@reader_thr = nil
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
@cv = ConditionVariable.new
|
|
31
|
+
@responses = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def open
|
|
35
|
+
return if @running
|
|
36
|
+
|
|
37
|
+
cmd_parts = if @args.empty? && @command.is_a?(String)
|
|
38
|
+
begin
|
|
39
|
+
Shellwords.split(@command)
|
|
40
|
+
rescue ArgumentError => e
|
|
41
|
+
raise ConnectionError, "Invalid command syntax: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
[@command.to_s] + @args
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
bin = cmd_parts.first
|
|
48
|
+
raise ConnectionError, "MCP command cannot be empty" if bin.nil? || bin.empty?
|
|
49
|
+
|
|
50
|
+
# Red-Team Defense: Verify command safety against destructive operations.
|
|
51
|
+
# Route the MCP spawn command through the same Guard#verify_command_safety!
|
|
52
|
+
# call that RunCommandTool uses — the narrow DANGEROUS_COMMAND_PATTERNS
|
|
53
|
+
# subset below was a bypass: it misses many dangerous commands the
|
|
54
|
+
# classifier flags. Fails closed on any dangerous/unmanaged command.
|
|
55
|
+
full_cmd = Shellwords.shelljoin(cmd_parts)
|
|
56
|
+
begin
|
|
57
|
+
@guard.verify_command_safety!(full_cmd)
|
|
58
|
+
rescue Rubik::Security::BlockedWriteError => e
|
|
59
|
+
# No manual audit here: verify_command_safety! already logged
|
|
60
|
+
# "blocked_command" with the TRUE classifier reason before raising
|
|
61
|
+
# — a second hardcoded entry would double-log and mislabel it.
|
|
62
|
+
raise ConnectionError, "Security blocked MCP process: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
resolved_bin = Rubik::Platform.which(bin)
|
|
66
|
+
cmd_parts[0] = resolved_bin if resolved_bin
|
|
67
|
+
|
|
68
|
+
string_env = @env.transform_keys(&:to_s).transform_values(&:to_s)
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
@stdin, @stdout, @stderr, @wait_thr = if string_env.empty?
|
|
72
|
+
Open3.popen3(*cmd_parts, chdir: @root_dir)
|
|
73
|
+
else
|
|
74
|
+
Open3.popen3(string_env, *cmd_parts, chdir: @root_dir)
|
|
75
|
+
end
|
|
76
|
+
rescue SystemCallError => e
|
|
77
|
+
raise ConnectionError, "Failed to spawn MCP process '#{bin}': #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@running = true
|
|
81
|
+
@req_id = 0
|
|
82
|
+
@responses.clear
|
|
83
|
+
|
|
84
|
+
start_reader_thread
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
alias start open
|
|
88
|
+
|
|
89
|
+
def send_request(method, params = {}, timeout: 10)
|
|
90
|
+
raise ConnectionError, "MCP transport is not running" unless @running && @stdin
|
|
91
|
+
|
|
92
|
+
req_id = @mutex.synchronize do
|
|
93
|
+
@req_id += 1
|
|
94
|
+
@req_id
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
msg = {
|
|
98
|
+
"jsonrpc" => "2.0",
|
|
99
|
+
"id" => req_id,
|
|
100
|
+
"method" => method.to_s,
|
|
101
|
+
"params" => params || {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
write_message(msg)
|
|
105
|
+
wait_for_response(req_id, timeout: timeout)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def send_notification(method, params = {})
|
|
109
|
+
raise ConnectionError, "MCP transport is not running" unless @running && @stdin
|
|
110
|
+
|
|
111
|
+
msg = {
|
|
112
|
+
"jsonrpc" => "2.0",
|
|
113
|
+
"method" => method.to_s,
|
|
114
|
+
"params" => params || {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
write_message(msg)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def close
|
|
121
|
+
return unless @running || @wait_thr&.alive?
|
|
122
|
+
|
|
123
|
+
@running = false
|
|
124
|
+
|
|
125
|
+
begin
|
|
126
|
+
@reader_thr&.kill
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
[@stdin, @stdout, @stderr].each do |io|
|
|
132
|
+
io&.close unless io&.closed?
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
@reader_thr&.join(0.5)
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
ensure
|
|
142
|
+
@reader_thr = nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if @wait_thr&.alive?
|
|
146
|
+
Rubik::Platform.kill_tree(@wait_thr.pid)
|
|
147
|
+
begin
|
|
148
|
+
@wait_thr.join(0.5)
|
|
149
|
+
rescue StandardError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
@wait_thr = nil
|
|
154
|
+
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
@responses.clear
|
|
157
|
+
@cv.broadcast
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
alias stop close
|
|
161
|
+
alias dispose close
|
|
162
|
+
|
|
163
|
+
def running?
|
|
164
|
+
@running && (@wait_thr.nil? || @wait_thr.alive?)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def pid
|
|
168
|
+
@wait_thr&.pid
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def write_message(msg_hash)
|
|
174
|
+
json = JSON.dump(msg_hash)
|
|
175
|
+
@mutex.synchronize do
|
|
176
|
+
raise ConnectionError, "Process stdin is closed" if @stdin.nil? || @stdin.closed?
|
|
177
|
+
|
|
178
|
+
@stdin.puts(json)
|
|
179
|
+
@stdin.flush
|
|
180
|
+
end
|
|
181
|
+
rescue IOError, Errno::EPIPE => e
|
|
182
|
+
close
|
|
183
|
+
raise ConnectionError, "Failed to write to MCP server: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def wait_for_response(req_id, timeout: 10)
|
|
187
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
188
|
+
response = @mutex.synchronize do
|
|
189
|
+
loop do
|
|
190
|
+
match = @responses.delete(req_id) || @responses.delete(req_id.to_s) || @responses.delete(req_id.to_i)
|
|
191
|
+
break match if match
|
|
192
|
+
raise ConnectionError, "MCP transport stopped while waiting for response" unless @running
|
|
193
|
+
|
|
194
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
195
|
+
remaining = timeout - elapsed
|
|
196
|
+
if remaining <= 0
|
|
197
|
+
@responses.delete(req_id)
|
|
198
|
+
raise TimeoutError, "Timed out waiting for MCP response to request #{req_id} after #{timeout}s"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
@cv.wait(@mutex, remaining)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if response["error"]
|
|
206
|
+
err = response["error"]
|
|
207
|
+
code = err.is_a?(Hash) ? err["code"] : nil
|
|
208
|
+
msg = err.is_a?(Hash) ? (err["message"] || err.to_s) : err.to_s
|
|
209
|
+
code_str = code ? " (#{code})" : ""
|
|
210
|
+
raise ProtocolError, "MCP error#{code_str}: #{msg}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
response["result"]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def start_reader_thread
|
|
217
|
+
@reader_thr = Thread.new do
|
|
218
|
+
while @running && @stdout && !@stdout.closed?
|
|
219
|
+
line = begin
|
|
220
|
+
@stdout.gets
|
|
221
|
+
rescue IOError, Errno::EBADF
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
break unless line
|
|
225
|
+
|
|
226
|
+
line = line.strip
|
|
227
|
+
next if line.empty?
|
|
228
|
+
|
|
229
|
+
begin
|
|
230
|
+
msg = JSON.parse(line)
|
|
231
|
+
handle_incoming_message(msg)
|
|
232
|
+
rescue JSON::ParserError
|
|
233
|
+
# Ignore non-JSON server logs, keep reader alive
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
ensure
|
|
237
|
+
@mutex.synchronize do
|
|
238
|
+
@running = false
|
|
239
|
+
@cv.broadcast
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def handle_incoming_message(msg)
|
|
245
|
+
return unless msg.is_a?(Hash) && msg.key?("id")
|
|
246
|
+
|
|
247
|
+
@mutex.synchronize do
|
|
248
|
+
@responses[msg["id"]] = msg
|
|
249
|
+
@cv.broadcast
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
data/lib/rubik/mcp.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
# Plan 61 D-15: low-cost per-run ledger of tool usage, touched file
|
|
6
|
+
# paths and tool errors, rendered into a compact turn summary. Failed
|
|
7
|
+
# or cancelled turns persist this summary next to their [Turn ...]
|
|
8
|
+
# marker so the model (and !resume) know what was already attempted.
|
|
9
|
+
# Loop owns an instance and resets it per run; StepExecutor records.
|
|
10
|
+
class ActivityLog
|
|
11
|
+
def initialize
|
|
12
|
+
@tools = Hash.new(0)
|
|
13
|
+
@files = []
|
|
14
|
+
@tool_errors = 0
|
|
15
|
+
@entries = [] # YENİ (Plan 85 D-2): sıralı per-call kronoloji [{ tool:, status: }, ...]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def record(tool_name, tool_args, status)
|
|
19
|
+
@tools[tool_name] += 1
|
|
20
|
+
@entries << { tool: tool_name, status: status } # YENİ: sıra korunur
|
|
21
|
+
path = tool_args["path"] || tool_args[:path]
|
|
22
|
+
@files << path.to_s if path && @files.size < 10 && !@files.include?(path.to_s)
|
|
23
|
+
@tool_errors += 1 if status == :error
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Rubik::Platform.debug_warn("ActivityLog#record failed (#{e.class}: #{e.message}); telemetry entry dropped")
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def summary(buffer_manager = nil)
|
|
30
|
+
parts = []
|
|
31
|
+
unless @tools.empty?
|
|
32
|
+
used = @tools.map { |name, count| count > 1 ? "#{name}(x#{count})" : name }
|
|
33
|
+
parts << "Tools used: #{used.join(', ')}"
|
|
34
|
+
end
|
|
35
|
+
parts << "Files touched: #{@files.join(', ')}" unless @files.empty?
|
|
36
|
+
parts << "Tool errors encountered: #{@tool_errors}" if @tool_errors.positive?
|
|
37
|
+
if buffer_manager&.dirty?
|
|
38
|
+
dirty = buffer_manager.dirty_files
|
|
39
|
+
names = dirty.is_a?(Hash) ? dirty.keys : Array(dirty)
|
|
40
|
+
parts << "Unapplied buffer changes: #{names.join(', ')}" unless names.empty?
|
|
41
|
+
end
|
|
42
|
+
parts.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# YENİ (Plan 85 D-2): Tüm tool çağrılarını gerçek kronolojik sırada döndürür.
|
|
46
|
+
# Model context'ine enjekte için tasarlandı. @entries boşsa "" döner.
|
|
47
|
+
def chronological_summary(max_entries: 20)
|
|
48
|
+
return "" if @entries.empty?
|
|
49
|
+
|
|
50
|
+
lines = @entries.first(max_entries).map.with_index(1) do |entry, i|
|
|
51
|
+
status_tag = entry[:status] == :error ? " [FAILED]" : ""
|
|
52
|
+
" #{i}. #{entry[:tool]}#{status_tag}"
|
|
53
|
+
end
|
|
54
|
+
lines << " ... (#{@entries.size - max_entries} more)" if @entries.size > max_entries
|
|
55
|
+
lines.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Kök-turn kronik satırı için kompakt araç dökümü (root-turn planı
|
|
59
|
+
# 2026-10-04): alfabetik ilk max_tools araç "ad(xN)" — sıralama
|
|
60
|
+
# sabit olduğundan replay'de aynı dize; @tools boşsa "".
|
|
61
|
+
def tool_tally(max_tools: 6)
|
|
62
|
+
return "" if @tools.empty?
|
|
63
|
+
|
|
64
|
+
@tools.keys.sort.first(max_tools).map { |name| "#{name}(x#{@tools[name]})" }.join(" ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def reset!
|
|
68
|
+
@tools.clear
|
|
69
|
+
@files.clear
|
|
70
|
+
@tool_errors = 0
|
|
71
|
+
@entries.clear # YENİ
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "subagent/notices"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Orchestrator
|
|
7
|
+
# Deep module (2026-09-22 review card 2): the ONE home for the buffer
|
|
8
|
+
# auto-apply policy. Eight call sites carried private spellings of the
|
|
9
|
+
# same decision (StepExecutor x3, subagent StepRunner x2, Supervisor
|
|
10
|
+
# completion, REPL !test/!sh) and drifted: the supervisor ignored
|
|
11
|
+
# buffer.auto_apply, subagent finish ignored on_finish, both subagent
|
|
12
|
+
# sites swallowed apply failures (tests could silently run on stale
|
|
13
|
+
# disk), the subagent steps skipped the AST sync, and the copy
|
|
14
|
+
# diverged ("file(s)" vs "modified file(s)").
|
|
15
|
+
#
|
|
16
|
+
# owns : the trigger vocabulary and decision table (mode x subagent
|
|
17
|
+
# flag x tool class), the canonical apply+sync effect, the
|
|
18
|
+
# failure-notice channel, config keys/defaults, and the REPL
|
|
19
|
+
# copy vocabulary
|
|
20
|
+
# leaves : the transport at each site (on_step event, event bus, IO),
|
|
21
|
+
# which buffer instance is applied, the branch-workspace
|
|
22
|
+
# fact, and branch merge semantics (Supervisor)
|
|
23
|
+
#
|
|
24
|
+
# Subagent state never enters this module (stateless-over-owner,
|
|
25
|
+
# ARCHITECTURE.md §2): config and trigger symbols in, booleans and
|
|
26
|
+
# strings out.
|
|
27
|
+
module AutoApply
|
|
28
|
+
MODE_KEY = "buffer.auto_apply" # config/defaults.rb: "manual"
|
|
29
|
+
FLAG_KEY = "subagents.auto_apply_buffer" # config/defaults.rb: true
|
|
30
|
+
|
|
31
|
+
# Defensive defaults for config-less hosts (tests, embedded). The
|
|
32
|
+
# flag default deliberately diverges from defaults.rb's true: a nil
|
|
33
|
+
# config reads as OFF so a config-less Supervisor never writes disk
|
|
34
|
+
# (the merge-without-apply pin, test_subagent_orchestration.rb).
|
|
35
|
+
# Pinned by name in test_auto_apply_policy.rb — do not "unify".
|
|
36
|
+
DEFAULT_MODE = "manual"
|
|
37
|
+
DEFAULT_FLAG = false
|
|
38
|
+
|
|
39
|
+
TEST_CLASS_TOOLS = ["run_command", "run_tests", "test", "lint"].freeze
|
|
40
|
+
|
|
41
|
+
# Trigger -> modes that fire it. on_test fires at finish on purpose:
|
|
42
|
+
# the mode ladder ends every turn flushed (historical StepExecutor
|
|
43
|
+
# semantics, preserved by the 2026-09-22 reconciliation). completion
|
|
44
|
+
# mirrors finish for the supervisor's post-merge moment.
|
|
45
|
+
TRIGGER_MODES = {
|
|
46
|
+
pre_execution: ["on_test", "always"],
|
|
47
|
+
finish: ["on_finish", "on_test", "always"],
|
|
48
|
+
completion: ["on_finish", "on_test", "always"],
|
|
49
|
+
post_tool: ["always"]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
# THE decision. subagent: true composes the subagents flag: flag on
|
|
55
|
+
# enables every trigger; otherwise the mode decides per trigger.
|
|
56
|
+
# tool classifies :pre_execution (nil = the caller already
|
|
57
|
+
# established the test-class context, e.g. REPL !test/!sh). mode:
|
|
58
|
+
# overrides config resolution entirely — the StepExecutor injection
|
|
59
|
+
# seam (its tests pass explicit modes positionally).
|
|
60
|
+
def apply?(config, trigger:, subagent: false, tool: nil, mode: nil)
|
|
61
|
+
return false if trigger == :pre_execution && !tool.nil? && !test_class_tool?(tool)
|
|
62
|
+
|
|
63
|
+
resolved = mode || (subagent && subagent_flag_on?(config) ? "always" : self.mode(config))
|
|
64
|
+
fires?(resolved, trigger)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Pure predicate on a resolved mode string.
|
|
68
|
+
def fires?(mode, trigger)
|
|
69
|
+
TRIGGER_MODES.fetch(trigger).include?(mode.to_s.downcase)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Raised when the disk apply succeeded but the context/AST index sync
|
|
73
|
+
# failed — the message says the changes ARE on disk so nobody
|
|
74
|
+
# "recovers" by re-applying a clean buffer.
|
|
75
|
+
class SyncFailedError < StandardError; end
|
|
76
|
+
|
|
77
|
+
# Canonical effect: apply the dirty buffer, then sync context/AST
|
|
78
|
+
# unless nothing was applied. Errors PROPAGATE (the main path
|
|
79
|
+
# contract); subagent callers rescue-and-report via report_failure.
|
|
80
|
+
def apply_and_sync(buffer_manager, shell)
|
|
81
|
+
return [] unless buffer_manager&.dirty?
|
|
82
|
+
|
|
83
|
+
applied = buffer_manager.apply!
|
|
84
|
+
begin
|
|
85
|
+
shell&.sync_context_and_ast(applied) unless applied.empty?
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
raise SyncFailedError, "index sync failed (changes ARE on disk): #{e.class}: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
applied
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Report-don't-swallow channel for the subagent paths. The trigger
|
|
93
|
+
# folds into reason; the cap keeps diagnostic color while preserving
|
|
94
|
+
# the TAIL — OS error messages carry the file path at the end, and a
|
|
95
|
+
# head-cut amputates exactly the actionable part. A failure channel
|
|
96
|
+
# must be the most defensive code on the path it protects: an emit
|
|
97
|
+
# that itself raises degrades to warn — raising here (from inside a
|
|
98
|
+
# caller's rescue) would mask the original apply error and, on the
|
|
99
|
+
# supervisor completion path, kill the COMPLETED_EVENT protocol.
|
|
100
|
+
REASON_CAP = 120
|
|
101
|
+
|
|
102
|
+
def report_failure(event_bus, id:, role:, state:, trigger:, reason:)
|
|
103
|
+
reason = capped_reason("#{trigger}: #{reason}")
|
|
104
|
+
begin
|
|
105
|
+
if event_bus.respond_to?(:emit)
|
|
106
|
+
Subagent::Notices.emit(event_bus, Subagent::Notices::AUTO_APPLY_FAILED_EVENT,
|
|
107
|
+
{ id: id, role: role, state: state.to_s, reason: reason })
|
|
108
|
+
else
|
|
109
|
+
warn("[AutoApply] subagent '#{id}' auto-apply (#{trigger}) failed: #{reason}")
|
|
110
|
+
end
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
warn("[AutoApply] subagent '#{id}' auto-apply (#{trigger}) failed: #{reason} " \
|
|
113
|
+
"(notice emit also failed: #{e.class}: #{e.message})")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# apply! does not raise on an external-modification conflict — it
|
|
118
|
+
# skips the file and records it — but a skip means the next run_tests
|
|
119
|
+
# executes against disk that lacks the buffered edit: the same
|
|
120
|
+
# stale-disk failure report_failure exists for. Call immediately
|
|
121
|
+
# after apply_and_sync; the post-apply dirty? check distinguishes "a
|
|
122
|
+
# skip just happened" from "buffer was already clean" (apply! rewrites
|
|
123
|
+
# last_conflicts on every run).
|
|
124
|
+
def report_conflicts(event_bus, buffer_manager, id:, role:, state:, trigger:)
|
|
125
|
+
return unless buffer_manager&.dirty?
|
|
126
|
+
|
|
127
|
+
conflicts = buffer_manager.respond_to?(:last_conflicts) ? Array(buffer_manager.last_conflicts) : []
|
|
128
|
+
return if conflicts.empty?
|
|
129
|
+
|
|
130
|
+
report_failure(event_bus, id: id, role: role, state: state, trigger: trigger,
|
|
131
|
+
reason: "conflict skipped: #{conflicts.join(', ')}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# -- copy vocabulary (the one home for auto-apply words) ------------
|
|
135
|
+
|
|
136
|
+
def applied_line(count, phase)
|
|
137
|
+
"[Rubik::Buffer] Auto-applied #{count} modified file(s) before #{phase}."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def unapplied_warning(subject)
|
|
141
|
+
"⚠️ [Rubik::Buffer] Notice: Unapplied buffer changes exist. #{subject} run against files on disk."
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# -- one defensive spelling each (replaced eight) -------------------
|
|
145
|
+
|
|
146
|
+
def mode(config)
|
|
147
|
+
config.respond_to?(:fetch) ? config.fetch(MODE_KEY, DEFAULT_MODE).to_s.downcase : DEFAULT_MODE
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def subagent_flag_on?(config)
|
|
151
|
+
config.respond_to?(:fetch) ? config.fetch(FLAG_KEY, DEFAULT_FLAG) == true : DEFAULT_FLAG
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_class_tool?(canonical_name)
|
|
155
|
+
TEST_CLASS_TOOLS.include?(canonical_name.to_s)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def capped_reason(reason)
|
|
159
|
+
reason = reason.to_s
|
|
160
|
+
reason.length > REASON_CAP ? reason[-REASON_CAP..] : reason
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private_class_method :capped_reason
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|