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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "diff/lcs"
|
|
4
|
+
require "diff/lcs/hunk"
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Buffer
|
|
8
|
+
class Diff
|
|
9
|
+
def self.unified_diff(file_path, original_content, new_content, context: 3)
|
|
10
|
+
orig_lines = (original_content || "").lines
|
|
11
|
+
new_lines = (new_content || "").lines
|
|
12
|
+
|
|
13
|
+
return "" if orig_lines == new_lines
|
|
14
|
+
|
|
15
|
+
diffs = ::Diff::LCS.diff(orig_lines, new_lines)
|
|
16
|
+
return "" if diffs.empty?
|
|
17
|
+
|
|
18
|
+
output = "--- a/#{file_path}\n+++ b/#{file_path}\n"
|
|
19
|
+
file_length_difference = 0
|
|
20
|
+
oldhunk = hunk = nil
|
|
21
|
+
|
|
22
|
+
diffs.each do |piece|
|
|
23
|
+
hunk = ::Diff::LCS::Hunk.new(orig_lines, new_lines, piece, context, file_length_difference)
|
|
24
|
+
file_length_difference = hunk.file_length_difference
|
|
25
|
+
next unless oldhunk
|
|
26
|
+
next if context.positive? && hunk.overlaps?(oldhunk) && hunk.merge(oldhunk)
|
|
27
|
+
|
|
28
|
+
output << oldhunk.diff(:unified) << "\n"
|
|
29
|
+
ensure
|
|
30
|
+
oldhunk = hunk
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if oldhunk
|
|
34
|
+
last = oldhunk.diff(:unified, true)
|
|
35
|
+
last << "\n" unless last.empty? || last.end_with?("\n")
|
|
36
|
+
output << last
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
output
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "monitor"
|
|
7
|
+
require_relative "diff"
|
|
8
|
+
require_relative "path_validator"
|
|
9
|
+
require_relative "../platform"
|
|
10
|
+
require_relative "../security/guard"
|
|
11
|
+
|
|
12
|
+
module Rubik
|
|
13
|
+
module Buffer
|
|
14
|
+
class InvalidPathError < StandardError; end
|
|
15
|
+
|
|
16
|
+
class Manager
|
|
17
|
+
attr_reader :root_dir, :dirty_files, :additional_roots, :rw_roots, :allowed_external_dirs, :rw_allowed_dirs, :guard, :last_conflicts
|
|
18
|
+
attr_accessor :allow_external_writes, :config
|
|
19
|
+
|
|
20
|
+
def initialize(root_dir: Dir.pwd, additional_roots: [], allowed_external_dirs: [], allow_external_writes: false,
|
|
21
|
+
event_bus: nil, guard: nil, config: nil)
|
|
22
|
+
@monitor = Monitor.new
|
|
23
|
+
@root_dir = File.expand_path(root_dir)
|
|
24
|
+
@additional_roots = (additional_roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
|
|
25
|
+
@allowed_external_dirs = (allowed_external_dirs || []).map { |d| File.expand_path(d.to_s, @root_dir) }.uniq
|
|
26
|
+
@allow_external_writes = allow_external_writes || false
|
|
27
|
+
@rw_roots = Set.new
|
|
28
|
+
@rw_allowed_dirs = Set.new(@allowed_external_dirs)
|
|
29
|
+
@event_bus = event_bus
|
|
30
|
+
@guard = guard || Rubik::Security::Guard.new
|
|
31
|
+
@config = config
|
|
32
|
+
@buffer = {}
|
|
33
|
+
@dirty_files = {}
|
|
34
|
+
@original_hashes = {}
|
|
35
|
+
@last_conflicts = []
|
|
36
|
+
@last_applied = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_root(path, rw: false) # rubocop:disable Naming/MethodParameterName -- rw: bilinçli kisa kwarg
|
|
40
|
+
@monitor.synchronize do
|
|
41
|
+
expanded = File.expand_path(path.to_s, @root_dir)
|
|
42
|
+
@additional_roots << expanded unless @additional_roots.include?(expanded)
|
|
43
|
+
if rw
|
|
44
|
+
@rw_roots << expanded
|
|
45
|
+
else
|
|
46
|
+
@rw_roots.delete(expanded)
|
|
47
|
+
end
|
|
48
|
+
expanded
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def remove_root(path)
|
|
53
|
+
@monitor.synchronize do
|
|
54
|
+
expanded = File.expand_path(path.to_s, @root_dir)
|
|
55
|
+
@additional_roots.delete(expanded)
|
|
56
|
+
@rw_roots.delete(expanded)
|
|
57
|
+
expanded
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_allowed_dir(path, rw: true) # rubocop:disable Naming/MethodParameterName -- rw: bilinçli kisa kwarg
|
|
62
|
+
@monitor.synchronize do
|
|
63
|
+
expanded = File.expand_path(path.to_s, @root_dir)
|
|
64
|
+
@allowed_external_dirs << expanded unless @allowed_external_dirs.include?(expanded)
|
|
65
|
+
if rw
|
|
66
|
+
@rw_allowed_dirs << expanded
|
|
67
|
+
else
|
|
68
|
+
@rw_allowed_dirs.delete(expanded)
|
|
69
|
+
end
|
|
70
|
+
expanded
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remove_allowed_dir(path)
|
|
75
|
+
@monitor.synchronize do
|
|
76
|
+
expanded = File.expand_path(path.to_s, @root_dir)
|
|
77
|
+
@allowed_external_dirs.delete(expanded)
|
|
78
|
+
@rw_allowed_dirs.delete(expanded)
|
|
79
|
+
expanded
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def sanitize_path(path, for_write: false)
|
|
84
|
+
expanded = File.expand_path(path.to_s, @root_dir)
|
|
85
|
+
|
|
86
|
+
# Boundary containment (symlink escape): lexical prefix matching is
|
|
87
|
+
# the floor; containment_root additionally resolves symlinks on BOTH
|
|
88
|
+
# sides, so a link planted inside the root cannot redirect
|
|
89
|
+
# reads/writes outside it. Stored buffer keys stay lexical.
|
|
90
|
+
matched_kind, matched_dir = containment_root(expanded)
|
|
91
|
+
|
|
92
|
+
unless matched_dir
|
|
93
|
+
all_allowed = ([@root_dir] + @additional_roots + @allowed_external_dirs).uniq
|
|
94
|
+
raise InvalidPathError,
|
|
95
|
+
"Path traversal attempt blocked: '#{path}'. Not within primary root, allowed workspace roots, " \
|
|
96
|
+
"or external allowed dirs: #{all_allowed.join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
matched_root = matched_kind == :root ? matched_dir : nil
|
|
100
|
+
matched_ext = matched_kind == :external ? matched_dir : nil
|
|
101
|
+
|
|
102
|
+
rel_or_exp = if matched_root && Rubik::Platform.path_equal?(matched_root, @root_dir)
|
|
103
|
+
begin
|
|
104
|
+
rel = Pathname.new(expanded).relative_path_from(Pathname.new(@root_dir)).to_s.gsub("\\", "/")
|
|
105
|
+
rel == "." ? "." : rel
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
expanded
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
expanded
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if for_write
|
|
114
|
+
# Critical path security check (@guard is always assigned in
|
|
115
|
+
# initialize — the historical re-fallback here was unreachable).
|
|
116
|
+
# Pass the ROOT-ANCHORED expanded path: the guard's lexical
|
|
117
|
+
# candidates and its symlink resolution both anchor to the process
|
|
118
|
+
# cwd, which diverges from the workspace root when the REPL is
|
|
119
|
+
# launched from elsewhere — resolving relative keys there would
|
|
120
|
+
# inspect the wrong tree.
|
|
121
|
+
begin
|
|
122
|
+
@guard.verify_path_write!(expanded)
|
|
123
|
+
rescue Rubik::Security::BlockedWriteError => e
|
|
124
|
+
raise InvalidPathError, e.message
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if matched_root && !Rubik::Platform.path_equal?(matched_root, @root_dir)
|
|
128
|
+
if !@allow_external_writes && @rw_roots.none? { |r| Rubik::Platform.path_equal?(r, matched_root) }
|
|
129
|
+
raise InvalidPathError,
|
|
130
|
+
"Write blocked on external workspace root: '#{path}'. Configure 'workspace.allow_external_writes: true' or add with --rw to enable writes."
|
|
131
|
+
end
|
|
132
|
+
elsif matched_ext
|
|
133
|
+
if !@allow_external_writes && @rw_allowed_dirs.none? { |d| Rubik::Platform.path_equal?(d, matched_ext) }
|
|
134
|
+
raise InvalidPathError,
|
|
135
|
+
"Write blocked on external allowed directory: '#{path}'. " \
|
|
136
|
+
"Configure 'workspace.allow_external_writes: true' or add with --rw to enable writes."
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
rel_or_exp
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Reads a file from the buffer (if dirty) or from disk, normalizing newlines.
|
|
145
|
+
#
|
|
146
|
+
# @param path [String] file path relative to the root directory
|
|
147
|
+
# @param reload [Boolean] force reload from disk, discarding in-memory buffer if present
|
|
148
|
+
# @param from_disk [Boolean] read directly from disk without modifying buffer or original hashes
|
|
149
|
+
# @return [String, nil] the file content, or nil if the file does not exist
|
|
150
|
+
def read(path, reload: false, from_disk: false)
|
|
151
|
+
@monitor.synchronize do
|
|
152
|
+
rel_path = sanitize_path(path, for_write: false)
|
|
153
|
+
if reload
|
|
154
|
+
@buffer.delete(rel_path)
|
|
155
|
+
@dirty_files.delete(rel_path)
|
|
156
|
+
@original_hashes.delete(rel_path)
|
|
157
|
+
@last_conflicts.delete(rel_path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if from_disk
|
|
161
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
162
|
+
return nil unless File.exist?(full_path)
|
|
163
|
+
|
|
164
|
+
return Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))
|
|
165
|
+
elsif @buffer.key?(rel_path)
|
|
166
|
+
return @buffer[rel_path]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
170
|
+
return nil unless File.exist?(full_path)
|
|
171
|
+
|
|
172
|
+
content = Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))
|
|
173
|
+
@original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
|
|
174
|
+
content
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def discard(path)
|
|
179
|
+
@monitor.synchronize do
|
|
180
|
+
rel_path = sanitize_path(path, for_write: false)
|
|
181
|
+
@buffer.delete(rel_path)
|
|
182
|
+
@dirty_files.delete(rel_path)
|
|
183
|
+
@original_hashes.delete(rel_path)
|
|
184
|
+
@last_conflicts.delete(rel_path)
|
|
185
|
+
@event_bus&.emit(:buffer_discarded, path: rel_path)
|
|
186
|
+
rel_path
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def write(path, content, reset_baseline: false)
|
|
191
|
+
@monitor.synchronize do
|
|
192
|
+
rel_path = sanitize_path(path, for_write: true)
|
|
193
|
+
normalized_content = Rubik::Platform.normalize_newlines(content)
|
|
194
|
+
|
|
195
|
+
if reset_baseline || !@original_hashes.key?(rel_path)
|
|
196
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
197
|
+
@original_hashes[rel_path] = (Digest::SHA256.hexdigest(Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))) if File.exist?(full_path))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
@buffer[rel_path] = normalized_content
|
|
201
|
+
@dirty_files[rel_path] = true
|
|
202
|
+
@event_bus&.emit(:buffer_changed, path: rel_path)
|
|
203
|
+
normalized_content
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Returns whether the buffer has any unapplied file modifications,
|
|
208
|
+
# or whether a specific file path is dirty or unwritten to disk.
|
|
209
|
+
#
|
|
210
|
+
# @param path [String, nil] optional path to check
|
|
211
|
+
# @return [Boolean] true if there are dirty files (or specified path is dirty)
|
|
212
|
+
def dirty?(path = nil)
|
|
213
|
+
@monitor.synchronize do
|
|
214
|
+
if path
|
|
215
|
+
rel_path = begin
|
|
216
|
+
sanitize_path(path, for_write: false)
|
|
217
|
+
rescue InvalidPathError
|
|
218
|
+
path.to_s
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return true if @dirty_files.key?(rel_path) || @dirty_files.key?(path.to_s)
|
|
222
|
+
|
|
223
|
+
target = if @buffer.key?(rel_path)
|
|
224
|
+
rel_path
|
|
225
|
+
elsif @buffer.key?(path.to_s)
|
|
226
|
+
path.to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if target
|
|
230
|
+
full_path = File.expand_path(target, @root_dir)
|
|
231
|
+
return true unless File.exist?(full_path)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
false
|
|
235
|
+
else
|
|
236
|
+
!@dirty_files.empty?
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns list of all known files in the buffer and on disk within root_dir.
|
|
242
|
+
# The disk walk happens OUTSIDE the monitor (review 2026-09-17, Finding 4):
|
|
243
|
+
# a full-tree Dir.glob under the lock used to block every concurrent
|
|
244
|
+
# buffer operation on large repos. We snapshot the in-buffer keys under
|
|
245
|
+
# the lock, glob without it, then merge — results may be a few ms stale
|
|
246
|
+
# for callers that only need a fuzzy-match candidate list.
|
|
247
|
+
def all_files
|
|
248
|
+
buffer_keys = @monitor.synchronize { @buffer.keys }
|
|
249
|
+
disk_files = begin
|
|
250
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: @root_dir).reject do |f|
|
|
251
|
+
f.start_with?(".git") || File.directory?(File.expand_path(f, @root_dir))
|
|
252
|
+
end
|
|
253
|
+
rescue SystemCallError
|
|
254
|
+
[]
|
|
255
|
+
end
|
|
256
|
+
(buffer_keys + disk_files).uniq
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def diff_all
|
|
260
|
+
snapshot = @monitor.synchronize do
|
|
261
|
+
@dirty_files.keys.map { |rel_path| [rel_path, @buffer[rel_path] || ""] }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
diffs = snapshot.map do |rel_path, new_content|
|
|
265
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
266
|
+
orig_content = File.exist?(full_path) ? Rubik::Platform.normalize_newlines(File.read(full_path)) : ""
|
|
267
|
+
Diff.unified_diff(rel_path, orig_content, new_content)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
diffs.join("\n")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def apply!(target_paths = nil, policy: nil)
|
|
274
|
+
@monitor.synchronize do
|
|
275
|
+
applied_paths = []
|
|
276
|
+
conflicts = []
|
|
277
|
+
keys_to_apply = if target_paths
|
|
278
|
+
Array(target_paths).map { |p| sanitize_path(p, for_write: true) }
|
|
279
|
+
else
|
|
280
|
+
@dirty_files.keys.dup
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
effective_policy = (policy || external_modification_policy).to_s.downcase
|
|
284
|
+
|
|
285
|
+
keys_to_apply.each do |rel_path|
|
|
286
|
+
next unless @dirty_files.key?(rel_path)
|
|
287
|
+
|
|
288
|
+
unless PathValidator.valid?(rel_path)
|
|
289
|
+
@dirty_files.delete(rel_path)
|
|
290
|
+
@buffer.delete(rel_path)
|
|
291
|
+
@original_hashes.delete(rel_path)
|
|
292
|
+
@event_bus&.emit(:buffer_garbage_skipped, path: rel_path)
|
|
293
|
+
next
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
baseline = @original_hashes[rel_path]
|
|
297
|
+
current_disk = disk_hash(rel_path)
|
|
298
|
+
|
|
299
|
+
conflict = if baseline
|
|
300
|
+
current_disk != baseline
|
|
301
|
+
else
|
|
302
|
+
!current_disk.nil?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if conflict && effective_policy != "overwrite"
|
|
306
|
+
conflicts << rel_path
|
|
307
|
+
@event_bus&.emit(:buffer_apply_conflict, paths: conflicts, path: rel_path)
|
|
308
|
+
next
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
312
|
+
|
|
313
|
+
# TOCTOU close: re-verify containment immediately before the
|
|
314
|
+
# atomic write — a symlink planted between write-time sanitize
|
|
315
|
+
# and apply! must not redirect the write outside the workspace.
|
|
316
|
+
# Fail loud: an untrustworthy path is an attack signal or a
|
|
317
|
+
# mid-flight root misconfiguration, not a content problem; the
|
|
318
|
+
# entry stays dirty for a retry after remediation.
|
|
319
|
+
containment_kind, = containment_root(full_path)
|
|
320
|
+
unless containment_kind
|
|
321
|
+
@event_bus&.emit(:buffer_containment_blocked, path: rel_path)
|
|
322
|
+
# Commit what this run has seen so "apply! rewrites
|
|
323
|
+
# last_conflicts on every run" holds for readers (AutoApply's
|
|
324
|
+
# report_conflicts) even on the failure path.
|
|
325
|
+
@last_conflicts = conflicts
|
|
326
|
+
raise InvalidPathError,
|
|
327
|
+
"Write blocked: '#{rel_path}' resolves outside all allowed roots at apply time " \
|
|
328
|
+
"(possible symlink swap since buffering). Re-verify the workspace and retry."
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
content = @buffer[rel_path]
|
|
332
|
+
|
|
333
|
+
Rubik::Platform.atomic_write(full_path, content)
|
|
334
|
+
|
|
335
|
+
@original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
|
|
336
|
+
@dirty_files.delete(rel_path)
|
|
337
|
+
@buffer.delete(rel_path)
|
|
338
|
+
applied_paths << rel_path
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
@last_conflicts = conflicts
|
|
342
|
+
@last_applied = { at: Time.now.utc, paths: applied_paths.dup.freeze } unless applied_paths.empty?
|
|
343
|
+
@event_bus&.emit(:buffer_applied, paths: applied_paths) unless applied_paths.empty?
|
|
344
|
+
applied_paths
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# When and which files the most recent apply! actually wrote to disk —
|
|
349
|
+
# lets apply_buffer distinguish "buffer was clean because the auto-apply
|
|
350
|
+
# policy already flushed these edits" from "nothing ever happened"
|
|
351
|
+
# (issue #97 finding 6). nil until the first successful apply.
|
|
352
|
+
def last_applied
|
|
353
|
+
@last_applied && { at: @last_applied[:at], paths: @last_applied[:paths] }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def reset!
|
|
357
|
+
@monitor.synchronize do
|
|
358
|
+
@buffer.clear
|
|
359
|
+
@dirty_files.clear
|
|
360
|
+
@original_hashes.clear
|
|
361
|
+
@last_conflicts.clear
|
|
362
|
+
@last_applied = nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def snapshot_state
|
|
367
|
+
@monitor.synchronize do
|
|
368
|
+
state = {}
|
|
369
|
+
@dirty_files.each_key do |rel_path|
|
|
370
|
+
state[rel_path] = @buffer[rel_path]
|
|
371
|
+
end
|
|
372
|
+
state
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def restore_snapshot_state(state)
|
|
377
|
+
@monitor.synchronize do
|
|
378
|
+
@buffer.clear
|
|
379
|
+
@dirty_files.clear
|
|
380
|
+
state.each do |rel_path, content|
|
|
381
|
+
@buffer[rel_path] = content
|
|
382
|
+
@dirty_files[rel_path] = true
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
389
|
+
# Single source of boundary containment for both the sanitize_path
|
|
390
|
+
# entry check and the apply! re-verification. Returns [kind, dir] with
|
|
391
|
+
# kind :root / :external (dir is the LEXICAL matched root/external
|
|
392
|
+
# dir — downstream keying stays lexical), or nil when the path,
|
|
393
|
+
# symlink-resolved on both sides, escapes every allowed root.
|
|
394
|
+
def containment_root(path)
|
|
395
|
+
allowed_roots = [@root_dir] + @additional_roots.map { |r| File.expand_path(r.to_s, @root_dir) }
|
|
396
|
+
root = allowed_roots.find { |r| Rubik::Platform.resolved_path_under?(path, r) }
|
|
397
|
+
return [:root, root] if root
|
|
398
|
+
|
|
399
|
+
ext = @allowed_external_dirs.map { |d| File.expand_path(d.to_s, @root_dir) }
|
|
400
|
+
.find { |d| Rubik::Platform.resolved_path_under?(path, d) }
|
|
401
|
+
ext && [:external, ext]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def external_modification_policy
|
|
405
|
+
raw = if @config.is_a?(Rubik::Config)
|
|
406
|
+
@config.fetch("buffer.on_external_modification", "skip")
|
|
407
|
+
elsif @config.respond_to?(:dig)
|
|
408
|
+
@config.dig("buffer", "on_external_modification") ||
|
|
409
|
+
(@config["buffer.on_external_modification"] if @config.respond_to?(:[])) ||
|
|
410
|
+
"skip"
|
|
411
|
+
else
|
|
412
|
+
"skip"
|
|
413
|
+
end
|
|
414
|
+
raw.to_s.downcase
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def disk_hash(rel_path)
|
|
418
|
+
full_path = File.expand_path(rel_path, @root_dir)
|
|
419
|
+
return nil unless File.exist?(full_path)
|
|
420
|
+
|
|
421
|
+
Digest::SHA256.hexdigest(Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path)))
|
|
422
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
423
|
+
nil
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Buffer
|
|
5
|
+
module PathValidator
|
|
6
|
+
# Regex for "simple filenames" — alphanumeric, hyphens, underscores only.
|
|
7
|
+
# Covers Gemfile, Dockerfile, LICENSE, README, Makefile, CHANGELOG, TODO,
|
|
8
|
+
# Vagrantfile, Procfile, and similar convention filenames without extensions.
|
|
9
|
+
SIMPLE_FILENAME_RE = /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
|
|
10
|
+
|
|
11
|
+
# Characters that are never valid in a file path — typically indicate LLM
|
|
12
|
+
# code fragment leakage (trailing comma from argument lists, leading ?
|
|
13
|
+
# from Ruby symbol syntax, shell wildcards, etc.).
|
|
14
|
+
INVALID_PATH_CHARS = [",", "?", "*", '"', "<", ">", "|", "\0"].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Checks whether a file path is valid for writing.
|
|
18
|
+
#
|
|
19
|
+
# A path is valid if it:
|
|
20
|
+
# 1. Is non-nil and non-blank
|
|
21
|
+
# 2. Contains no invalid characters (see INVALID_PATH_CHARS)
|
|
22
|
+
# 3. Matches at least one positive pattern:
|
|
23
|
+
# a. A "simple filename" (alphanumeric + hyphens/underscores only)
|
|
24
|
+
# b. Contains a dot followed by at least one character (extension)
|
|
25
|
+
# c. Contains a path separator (/ or \)
|
|
26
|
+
#
|
|
27
|
+
# @param path [String, nil] the path to validate
|
|
28
|
+
# @return [Boolean] true if the path is valid
|
|
29
|
+
def valid?(path)
|
|
30
|
+
return false if path.nil? || path.to_s.strip.empty?
|
|
31
|
+
|
|
32
|
+
cleaned = path.to_s.strip
|
|
33
|
+
|
|
34
|
+
# 1. NEGATIVE CHECK: Reject paths with invalid characters.
|
|
35
|
+
# Runs BEFORE positive checks — catches LLM code fragment leakage
|
|
36
|
+
# (trailing comma, leading question mark, etc.).
|
|
37
|
+
return false if contains_invalid_chars?(cleaned)
|
|
38
|
+
|
|
39
|
+
# 2. Simple filename (alphanumeric + hyphens/underscores, no extension)
|
|
40
|
+
return true if SIMPLE_FILENAME_RE.match?(cleaned)
|
|
41
|
+
|
|
42
|
+
# 3. Valid file extension — contains a dot with at least 1 char after
|
|
43
|
+
return true if cleaned.include?(".") && !cleaned.end_with?(".")
|
|
44
|
+
|
|
45
|
+
# 4. Contains a path separator
|
|
46
|
+
return true if cleaned.include?("/") || cleaned.include?("\\")
|
|
47
|
+
|
|
48
|
+
# 5. No condition matched — garbage
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def contains_invalid_chars?(cleaned)
|
|
55
|
+
cleaned.chars.any? { |c| INVALID_PATH_CHARS.include?(c) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Buffer
|
|
10
|
+
class Snapshot
|
|
11
|
+
attr_reader :id, :timestamp, :files
|
|
12
|
+
|
|
13
|
+
def initialize(id: nil, timestamp: nil, files: {})
|
|
14
|
+
@id = id || SecureRandom.hex(6)
|
|
15
|
+
@timestamp = timestamp || Time.now.to_i
|
|
16
|
+
@files = files
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
"id" => @id,
|
|
22
|
+
"timestamp" => @timestamp,
|
|
23
|
+
"files" => @files
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_h(hash)
|
|
28
|
+
new(
|
|
29
|
+
id: hash["id"],
|
|
30
|
+
timestamp: hash["timestamp"],
|
|
31
|
+
files: hash["files"] || {}
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def compress
|
|
36
|
+
Zlib::Deflate.deflate(JSON.dump(to_h))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.decompress(binary_data)
|
|
40
|
+
decompressed = Zlib::Inflate.inflate(binary_data)
|
|
41
|
+
from_h(JSON.parse(decompressed))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class SnapshotStore
|
|
46
|
+
attr_reader :storage_dir, :snapshots
|
|
47
|
+
|
|
48
|
+
def initialize(storage_dir: File.join(ENV.fetch("HOME", nil), ".rubik", "snapshots", "default"))
|
|
49
|
+
@storage_dir = storage_dir
|
|
50
|
+
@snapshots = []
|
|
51
|
+
@current_index = -1
|
|
52
|
+
FileUtils.mkdir_p(@storage_dir)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create_snapshot(buffer_manager)
|
|
56
|
+
return nil unless buffer_manager.dirty?
|
|
57
|
+
|
|
58
|
+
snapshot = Snapshot.new(files: buffer_manager.snapshot_state)
|
|
59
|
+
# Naming reflects the actual codec: this is Zlib::Deflate (RFC 1951),
|
|
60
|
+
# NOT Zstandard despite the "z" prefix. "zst" was misleading and could
|
|
61
|
+
# fool a tool into trying a zstd decompressor. Kept short to preserve
|
|
62
|
+
# the existing file layout.
|
|
63
|
+
file_path = File.join(@storage_dir, "#{snapshot.id}.zdeflate")
|
|
64
|
+
File.binwrite(file_path, snapshot.compress)
|
|
65
|
+
|
|
66
|
+
@snapshots = @snapshots[0..@current_index] if @current_index < @snapshots.size - 1
|
|
67
|
+
|
|
68
|
+
@snapshots << snapshot
|
|
69
|
+
@current_index = @snapshots.size - 1
|
|
70
|
+
snapshot
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def undo(buffer_manager)
|
|
74
|
+
return false if @current_index.negative?
|
|
75
|
+
|
|
76
|
+
if @current_index.zero?
|
|
77
|
+
buffer_manager.restore_snapshot_state({})
|
|
78
|
+
@current_index = -1
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@current_index -= 1
|
|
83
|
+
prev_snapshot = @snapshots[@current_index]
|
|
84
|
+
buffer_manager.restore_snapshot_state(prev_snapshot.files)
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rollback(snapshot_id, buffer_manager)
|
|
89
|
+
target_idx = @snapshots.index { |s| s.id == snapshot_id }
|
|
90
|
+
return false unless target_idx
|
|
91
|
+
|
|
92
|
+
target_snapshot = @snapshots[target_idx]
|
|
93
|
+
buffer_manager.restore_snapshot_state(target_snapshot.files)
|
|
94
|
+
@current_index = target_idx
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|