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,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Security
|
|
5
|
+
# Situation-specific prose for a blocked command. The evidence and the
|
|
6
|
+
# gate-honest policy (which allowlist entry would open the gate) arrive
|
|
7
|
+
# as plain data on BlockedWriteError; this module only formats. It lives
|
|
8
|
+
# in the security domain, next to the error it renders —
|
|
9
|
+
# RunCommandTool.blocked_guidance is the stable public seam.
|
|
10
|
+
#
|
|
11
|
+
# Contract for every variant: the FIRST line starts with the literal
|
|
12
|
+
# "Security blocked:" and stays single-line (StepExecutor::ERROR_PREFIXES
|
|
13
|
+
# classifies by start_with?; the REPL/UI formatters collapse to the first
|
|
14
|
+
# line plus the first "!allow-cmd|!allow-dir" line they can regex out).
|
|
15
|
+
module BlockedGuidance
|
|
16
|
+
# Render caps for the multi-segment variant: how many dangerous
|
|
17
|
+
# segments and passed siblings to list, and how long the collapsed
|
|
18
|
+
# command preview on the first line may be.
|
|
19
|
+
SEGMENT_HINT_LIMIT = 5
|
|
20
|
+
SAFE_SEGMENT_LIMIT = 8
|
|
21
|
+
BLOCKED_PREVIEW_CAP = 80
|
|
22
|
+
|
|
23
|
+
# The operator-facing allowlist syntax is always a copy-pasteable
|
|
24
|
+
# one-liner: `!allow-cmd add "<command>"`.
|
|
25
|
+
def self.render(err)
|
|
26
|
+
cmd = err.respond_to?(:blocked_command) ? err.blocked_command : nil
|
|
27
|
+
rule = err.respond_to?(:rule_name) ? err.rule_name : nil
|
|
28
|
+
kind = err.respond_to?(:kind) ? err.kind : nil
|
|
29
|
+
quoted = cmd ? "\"#{cmd}\"" : nil
|
|
30
|
+
# Unstructured raises (critical-write) carry all their context in
|
|
31
|
+
# #message — never discard it behind "rule: unknown".
|
|
32
|
+
message = err.message
|
|
33
|
+
structural = cmd || rule
|
|
34
|
+
|
|
35
|
+
lines = if kind == :chdir_outside_root
|
|
36
|
+
# Sandbox path fence: the working gate is !allow-dir, not
|
|
37
|
+
# !allow-cmd — point the operator at the right one. Structured
|
|
38
|
+
# kind:/path: from Runner#verify_chdir_safety! (no message
|
|
39
|
+
# regex); the '<directory>' fallback covers a hypothetical
|
|
40
|
+
# field-less raise.
|
|
41
|
+
path = err.respond_to?(:path) ? err.path : nil
|
|
42
|
+
[
|
|
43
|
+
"Security blocked: #{message}",
|
|
44
|
+
"If intended, the operator can extend the sandbox once:",
|
|
45
|
+
" !allow-dir add \"#{path || '<directory>'}\""
|
|
46
|
+
]
|
|
47
|
+
elsif !structural && message
|
|
48
|
+
[
|
|
49
|
+
"Security blocked: #{message}",
|
|
50
|
+
"If genuinely required and safe, the operator can allowlist it once:"
|
|
51
|
+
]
|
|
52
|
+
elsif kind == :unknown_command
|
|
53
|
+
# Database-less binary (fail-closed), NOT an interpreter
|
|
54
|
+
# blocklist hit — the probe variant below would mislabel
|
|
55
|
+
# it ("base rule for shell interpreters").
|
|
56
|
+
[
|
|
57
|
+
"Security blocked: #{message}",
|
|
58
|
+
"The binary is not in the command database, so its effects could not be classified.",
|
|
59
|
+
"If genuinely required and safe, the operator can allowlist it once:",
|
|
60
|
+
" !allow-cmd add #{quoted}"
|
|
61
|
+
]
|
|
62
|
+
elsif cmd && err.respond_to?(:probe_like) && err.probe_like
|
|
63
|
+
# Probe branch first: `python3 --version` is probe-shaped and
|
|
64
|
+
# must not be hijacked by the text-munging variant below.
|
|
65
|
+
[
|
|
66
|
+
"Security blocked: `#{cmd}` is blocklisted by the base rule for shell interpreters, even for version checks.",
|
|
67
|
+
"This looks like a safe read-only probe. If intended, the operator can allowlist it once:",
|
|
68
|
+
" !allow-cmd add #{quoted}"
|
|
69
|
+
]
|
|
70
|
+
elsif (segment_lines = dangerous_segment_lines(err, cmd))
|
|
71
|
+
segment_lines
|
|
72
|
+
elsif (expression_lines = expression_elevation_lines(err, cmd))
|
|
73
|
+
expression_lines
|
|
74
|
+
elsif text_munging?(cmd)
|
|
75
|
+
[
|
|
76
|
+
"Security blocked: shell text-munging is not allowed for file edits.",
|
|
77
|
+
"Use the native tools instead: read_file → replace_file_content / write_file,",
|
|
78
|
+
"then verify with lsp_diagnostics or run_tests.",
|
|
79
|
+
"If genuinely required, the operator can allowlist the exact command:",
|
|
80
|
+
" !allow-cmd add #{quoted}"
|
|
81
|
+
]
|
|
82
|
+
else
|
|
83
|
+
[
|
|
84
|
+
"Security blocked: `#{cmd || 'command'}` was denied (rule: #{rule || 'unknown'}).",
|
|
85
|
+
"If genuinely required and safe, the operator can allowlist it once:",
|
|
86
|
+
" !allow-cmd add #{quoted}"
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Never print the generic text-munging boilerplate for probe scenarios.
|
|
91
|
+
lines.compact.join("\n")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Wrapper / multi-segment variant: names EVERY dangerous segment with
|
|
95
|
+
# its rule and trigger instead of blaming the whole script on the first
|
|
96
|
+
# rule hit (a wrapped `bash -c '...'` used to render as one giant
|
|
97
|
+
# multi-line `cmd` plus "rule: git.merge" while the real triggers —
|
|
98
|
+
# `git merge --abort`, `git branch -D` — were discarded). Rendered when
|
|
99
|
+
# the danger lives inside a wrapper (depth > 0), spans several
|
|
100
|
+
# top-level commands, or shares the line with safe top-level siblings
|
|
101
|
+
# (the gate demands an entry for them too, and the concise variant
|
|
102
|
+
# would suggest the un-allowlistable compound string). A lone dangerous
|
|
103
|
+
# top-level command keeps the concise one-rule message. Returns nil so
|
|
104
|
+
# the caller falls through.
|
|
105
|
+
def self.dangerous_segment_lines(err, cmd)
|
|
106
|
+
report = err.respond_to?(:segment_report) ? err.segment_report : nil
|
|
107
|
+
return nil unless report.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
segments = Array(report[:segments]).grep(Hash)
|
|
110
|
+
return nil if segments.empty?
|
|
111
|
+
return nil if segments.size == 1 && segments.first[:depth].to_i.zero? && Array(report[:safe]).empty?
|
|
112
|
+
|
|
113
|
+
wrapper = report[:wrapper_preview]
|
|
114
|
+
preview = wrapper || cmd.to_s.gsub(/\s+/, " ").strip
|
|
115
|
+
preview = "command" if preview.empty?
|
|
116
|
+
preview = "#{preview[0, BLOCKED_PREVIEW_CAP - 1]}..." if preview.length > BLOCKED_PREVIEW_CAP
|
|
117
|
+
|
|
118
|
+
count = segments.size
|
|
119
|
+
plural = count == 1 ? "" : "s"
|
|
120
|
+
lines = ["Security blocked: #{count} dangerous segment#{plural} in `#{preview}` — nothing was executed."]
|
|
121
|
+
segments.first(SEGMENT_HINT_LIMIT).each_with_index do |s, i|
|
|
122
|
+
lines << " #{i + 1}. #{s[:display]} (rule: #{segment_annotation(s)})"
|
|
123
|
+
end
|
|
124
|
+
lines << " ...and #{count - SEGMENT_HINT_LIMIT} more dangerous segment#{plural}" if count > SEGMENT_HINT_LIMIT
|
|
125
|
+
|
|
126
|
+
safe = Array(report[:safe]).map(&:to_s).reject(&:empty?)
|
|
127
|
+
unless safe.empty?
|
|
128
|
+
shown = safe.first(SAFE_SEGMENT_LIMIT).join(", ")
|
|
129
|
+
shown += ", ..." if safe.size > SAFE_SEGMENT_LIMIT
|
|
130
|
+
lines << "Everything else in the #{wrapper ? 'script' : 'command'} passed classification (#{shown})."
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
entries = Array(report[:allowlist_entries]).map(&:to_s).reject(&:empty?)
|
|
134
|
+
lines << "Options: drop the blocked segment#{plural} and rerun each safe command directly, " \
|
|
135
|
+
"or the operator can allowlist #{wrapper ? 'once' : 'each one'}:"
|
|
136
|
+
entries.first(SEGMENT_HINT_LIMIT).each { |e| lines << %( !allow-cmd add "#{e}") }
|
|
137
|
+
lines << "(segment-level entries only cover top-level commands, not wrapped scripts)" if wrapper
|
|
138
|
+
lines
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Trigger annotation for one segment. The option form is used ONLY
|
|
142
|
+
# when the classifier reason confirms the override reached DANGEROUS —
|
|
143
|
+
# an option may override to a milder class (curl -o → external) while
|
|
144
|
+
# the real trigger is a system-path elevation named in the reason.
|
|
145
|
+
def self.segment_annotation(segment)
|
|
146
|
+
rule = segment[:rule] || "unknown"
|
|
147
|
+
reason = segment[:reason].to_s.strip
|
|
148
|
+
return "#{rule} — option #{segment[:option]} overrides to DANGEROUS" if segment[:option] && reason.match?(/overridden by option .+ to dangerous\z/i)
|
|
149
|
+
|
|
150
|
+
# Base-rule reasons restate matched_rule ("base rule: rm (…)") — the
|
|
151
|
+
# rule is already displayed; keep only the elevation evidence.
|
|
152
|
+
reason = reason.sub(/\A(?:blocked by default |base )rule:[^;]*;?\s*/i, "").strip
|
|
153
|
+
reason.empty? ? rule : "#{rule} — #{reason}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Expression-level variant: the danger emerges from the pipeline's
|
|
157
|
+
# shape (a mutating step reaching a system directory), not from any
|
|
158
|
+
# single dangerous segment — no segment report exists and NO allowlist
|
|
159
|
+
# entry would honestly open the gate, so steer toward restructuring
|
|
160
|
+
# instead of `!allow-cmd`.
|
|
161
|
+
def self.expression_elevation_lines(err, cmd)
|
|
162
|
+
return nil if err.respond_to?(:segment_report) && err.segment_report
|
|
163
|
+
|
|
164
|
+
reason = err.respond_to?(:classification_reason) ? err.classification_reason.to_s.strip : ""
|
|
165
|
+
return nil if reason.empty?
|
|
166
|
+
|
|
167
|
+
preview = cmd.to_s.gsub(/\s+/, " ").strip
|
|
168
|
+
preview = "command" if preview.empty?
|
|
169
|
+
preview = "#{preview[0, BLOCKED_PREVIEW_CAP - 1]}..." if preview.length > BLOCKED_PREVIEW_CAP
|
|
170
|
+
|
|
171
|
+
[
|
|
172
|
+
"Security blocked: `#{preview}` — nothing was executed.",
|
|
173
|
+
"#{reason}.",
|
|
174
|
+
"Restructure the pipeline so no mutating step touches a system directory —",
|
|
175
|
+
"stage intermediate output under /tmp, or run the steps as separate commands."
|
|
176
|
+
]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# (b) variant trigger: file-editing via text-munging interpreters.
|
|
180
|
+
def self.text_munging?(cmd)
|
|
181
|
+
return false if cmd.to_s.strip.empty?
|
|
182
|
+
|
|
183
|
+
["sed", "awk", "perl", "python3", "python", "ruby"].include?(cmd.split(/\s+/).first.to_s.downcase)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "models"
|
|
4
|
+
require_relative "database"
|
|
5
|
+
require_relative "parser"
|
|
6
|
+
require_relative "matcher"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Security
|
|
10
|
+
module Classifier
|
|
11
|
+
class Engine
|
|
12
|
+
SYSTEM_DIRS = [
|
|
13
|
+
"/etc", "/lib", "/lib64", "/usr", "/bin", "/sbin", "/boot", "/sys",
|
|
14
|
+
"/proc", "/run", "/srv", "/root", "/opt", "/var", "/dev"
|
|
15
|
+
].freeze
|
|
16
|
+
# How many mutating pipeline participants an expression-level danger
|
|
17
|
+
# reason names before eliding the rest — a hint, not a transcript.
|
|
18
|
+
EXPRESSION_MUTATOR_CAP = 3
|
|
19
|
+
SAFE_PREFIXES = [
|
|
20
|
+
"/tmp", "/var/tmp", "/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/fd", "/dev/tcp", "/dev/udp"
|
|
21
|
+
].freeze
|
|
22
|
+
DEV_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/fd/"].freeze
|
|
23
|
+
WRITE_OPERATORS = [">", ">>", "2>", "&>", ">&", "2>>", "*>"].freeze
|
|
24
|
+
READ_OPERATORS = ["<"].freeze
|
|
25
|
+
|
|
26
|
+
def self.default_database
|
|
27
|
+
@default_database ||= CommandDatabase.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.classify_expression(expression, database: nil, shell: nil)
|
|
31
|
+
new(database: database, shell: shell).classify(expression)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(database: nil, shell: nil)
|
|
35
|
+
@database = database || self.class.default_database
|
|
36
|
+
@shell = shell
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def classify(expression)
|
|
40
|
+
invocations, parse_warnings = Parser.parse(expression, shell: @shell)
|
|
41
|
+
command_results = []
|
|
42
|
+
all_redirects = []
|
|
43
|
+
|
|
44
|
+
invocations.each do |inv|
|
|
45
|
+
result = classify_invocation(inv, parse_warnings)
|
|
46
|
+
command_results << result
|
|
47
|
+
all_redirects.concat(inv.redirects)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
build_expression_result(expression, command_results, all_redirects, parse_warnings)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def classify_invocation(inv, parse_warnings)
|
|
56
|
+
result = if inv.argv.any? && inv.argv[0].start_with?("$")
|
|
57
|
+
CommandResult.new(
|
|
58
|
+
command: [inv.argv[0]], argv: inv.argv.dup, classification: Classification::DANGEROUS,
|
|
59
|
+
risk: Risk::HIGH, matched_rule: nil, inner_commands: [],
|
|
60
|
+
classification_reason: "variable expansion in command position"
|
|
61
|
+
)
|
|
62
|
+
else
|
|
63
|
+
Matcher.match_command(inv, @database, parse_warnings, shell: @shell)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pre-command env assignments (VAR="value cmd"): the wrapped
|
|
67
|
+
# command may execute the VALUE (GIT_SSH_COMMAND) — classify it
|
|
68
|
+
# as a sibling inner command BEFORE redirects/system-path checks
|
|
69
|
+
# so the rest of the pipeline sees the elevation.
|
|
70
|
+
result = apply_assignment_payloads(result, inv, parse_warnings)
|
|
71
|
+
|
|
72
|
+
write_paths, read_paths, all_temp = extract_paths(inv.redirects)
|
|
73
|
+
result = apply_redirects(result, inv.redirects, write_paths, all_temp)
|
|
74
|
+
result = apply_network_checks(result, inv)
|
|
75
|
+
result = apply_backgrounding(result, inv)
|
|
76
|
+
result = apply_system_path_elevation(result, inv)
|
|
77
|
+
|
|
78
|
+
result.with(write_paths: (write_paths.empty? ? nil : write_paths),
|
|
79
|
+
read_paths: (read_paths.empty? ? nil : read_paths))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `GIT_SSH_COMMAND="node -e evil" git push` — git executes the value
|
|
83
|
+
# as a shell command; `FOO=$(rm -rf /) cmd` substitutes one in. The
|
|
84
|
+
# VALUE becomes a classification input through the same inner-command
|
|
85
|
+
# machinery delegation uses, while the outer argv stays untouched so
|
|
86
|
+
# allowlist entries never derive from assignment payloads.
|
|
87
|
+
def apply_assignment_payloads(result, inv, parse_warnings)
|
|
88
|
+
return result if inv.assignments.empty?
|
|
89
|
+
|
|
90
|
+
inners = inv.assignments.filter_map do |assignment|
|
|
91
|
+
name, value = assignment.split("=", 2)
|
|
92
|
+
value = Matcher::Delegation.strip_quotes(value.to_s)
|
|
93
|
+
next nil if value.strip.empty?
|
|
94
|
+
|
|
95
|
+
inner_invs, warnings = Parser.parse(value, shell: @shell)
|
|
96
|
+
Matcher::Delegation.record_warnings(parse_warnings, warnings)
|
|
97
|
+
next nil unless command_shaped?(inner_invs)
|
|
98
|
+
|
|
99
|
+
inner_invs.map do |inner_inv|
|
|
100
|
+
Matcher::Delegation.match_inner_command(
|
|
101
|
+
inner_inv.argv, @database,
|
|
102
|
+
delegation_mode: "env_assignment",
|
|
103
|
+
delegation_source: name,
|
|
104
|
+
parse_warnings: parse_warnings,
|
|
105
|
+
redirects: inner_inv.redirects
|
|
106
|
+
) { |i, db, w| Matcher.match_command(i, db, w, shell: @shell) }
|
|
107
|
+
end
|
|
108
|
+
end.flatten
|
|
109
|
+
return result if inners.empty?
|
|
110
|
+
|
|
111
|
+
classification = result.classification
|
|
112
|
+
risk = result.risk
|
|
113
|
+
reason = result.classification_reason
|
|
114
|
+
inners.each do |inner|
|
|
115
|
+
if Classification.severity(inner.classification) > Classification.severity(classification)
|
|
116
|
+
classification = Classification.max_severity(classification, inner.classification)
|
|
117
|
+
reason = reason ? "#{reason}; elevated by env assignment payload" : "elevated by env assignment payload"
|
|
118
|
+
end
|
|
119
|
+
risk = Risk.max_severity(risk, inner.risk)
|
|
120
|
+
end
|
|
121
|
+
result.with(inner_commands: result.inner_commands + inners,
|
|
122
|
+
classification: classification, risk: risk, classification_reason: reason)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# False-positive firewall: env values are usually scalars (LANG=C)
|
|
126
|
+
# or config blobs (CFLAGS="-O2 -Wall", PS1="\u@\h"). A value counts
|
|
127
|
+
# as a command payload only when it parses into an invocation whose
|
|
128
|
+
# binary the database KNOWS or that is invoked by path — the same
|
|
129
|
+
# trust boundary the matcher applies to any other argv[0].
|
|
130
|
+
# Known residual: unknown-binary payloads (a DB-less interpreter
|
|
131
|
+
# spelled without a path) stay unclassified, at parity with typing
|
|
132
|
+
# that binary directly (UNKNOWN). Closing that would need a
|
|
133
|
+
# known-env-exec-binaries list (GIT_SSH_COMMAND, GIT_EDITOR, SHELL).
|
|
134
|
+
def command_shaped?(inner_invs)
|
|
135
|
+
inner_invs.any? do |inv|
|
|
136
|
+
binary = inv.argv.first.to_s
|
|
137
|
+
!binary.empty? && !binary.start_with?("-") &&
|
|
138
|
+
(@database.get(binary, shell: @shell) || binary.include?("/") || binary.include?("\\"))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_paths(redirects)
|
|
143
|
+
write_paths = []
|
|
144
|
+
read_paths = []
|
|
145
|
+
all_temp = true
|
|
146
|
+
|
|
147
|
+
redirects.each do |red|
|
|
148
|
+
if WRITE_OPERATORS.include?(red.operator) && real_file_path?(red.target)
|
|
149
|
+
write_paths << red.target
|
|
150
|
+
all_temp = false unless temp_path?(red.target)
|
|
151
|
+
elsif READ_OPERATORS.include?(red.operator) && real_file_path?(red.target)
|
|
152
|
+
read_paths << red.target
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
[write_paths, read_paths, all_temp]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def apply_redirects(result, redirects, write_paths, all_temp)
|
|
159
|
+
c = result.classification
|
|
160
|
+
r = result.risk
|
|
161
|
+
reason = result.classification_reason
|
|
162
|
+
|
|
163
|
+
redirects.each do |red|
|
|
164
|
+
if red.affects_classification
|
|
165
|
+
elevated = Classification.max_severity(c, Classification::LOCAL_EFFECTS)
|
|
166
|
+
if elevated != c
|
|
167
|
+
c = elevated
|
|
168
|
+
reason = reason ? "#{reason}; elevated by output redirect" : "elevated by output redirect"
|
|
169
|
+
end
|
|
170
|
+
r = Risk.max_severity(r, Risk::MEDIUM) unless write_paths.any? && all_temp
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
next unless red.target.start_with?("/dev/tcp/") || red.target.start_with?("/dev/udp/")
|
|
174
|
+
|
|
175
|
+
c = Classification::DANGEROUS
|
|
176
|
+
r = Risk::HIGH
|
|
177
|
+
reason = "elevated to DANGEROUS: /dev/tcp or /dev/udp access detected"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result.with(classification: c, risk: r, classification_reason: reason)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def apply_network_checks(result, inv)
|
|
184
|
+
if inv.argv.any? { |arg| arg.start_with?("/dev/tcp/") || arg.start_with?("/dev/udp/") }
|
|
185
|
+
return result.with(
|
|
186
|
+
classification: Classification::DANGEROUS, risk: Risk::HIGH,
|
|
187
|
+
classification_reason: "elevated to DANGEROUS: /dev/tcp or /dev/udp access detected"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
result
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def apply_backgrounding(result, inv)
|
|
194
|
+
return result unless inv.is_background
|
|
195
|
+
|
|
196
|
+
c = Classification.max_severity(result.classification, Classification::LOCAL_EFFECTS)
|
|
197
|
+
r = Risk.max_severity(result.risk, Risk::MEDIUM)
|
|
198
|
+
reason = result.classification_reason ? "#{result.classification_reason}; elevated by backgrounding" : "elevated by backgrounding"
|
|
199
|
+
result.with(classification: c, risk: r, classification_reason: reason)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def apply_system_path_elevation(result, inv)
|
|
203
|
+
updated_inners = result.inner_commands.map { |inner| check_inner_system_paths(inner) }
|
|
204
|
+
result = result.with(inner_commands: updated_inners) if updated_inners != result.inner_commands
|
|
205
|
+
|
|
206
|
+
if result.inner_commands.any? { |i| i.classification == Classification::DANGEROUS }
|
|
207
|
+
reason = "#{result.classification_reason}; elevated to DANGEROUS by inner command"
|
|
208
|
+
result = result.with(classification: Classification::DANGEROUS, risk: Risk::HIGH, classification_reason: reason)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return result if Classification.severity(result.classification) < Classification.severity(Classification::LOCAL_EFFECTS)
|
|
212
|
+
|
|
213
|
+
sys_paths = inv.argv.select { |t| system_path?(t) }
|
|
214
|
+
sys_paths.concat(inv.redirects.map(&:target).select { |t| system_path?(t) })
|
|
215
|
+
|
|
216
|
+
if sys_paths.any? && result.classification != Classification::DANGEROUS
|
|
217
|
+
reason = "#{result.classification_reason}; elevated to DANGEROUS: system path #{sys_paths[0]}"
|
|
218
|
+
result = result.with(classification: Classification::DANGEROUS, risk: Risk::HIGH, classification_reason: reason)
|
|
219
|
+
end
|
|
220
|
+
result
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def check_inner_system_paths(inner)
|
|
224
|
+
# Redirect floor BEFORE the severity gate: an `echo ... > /etc/x`
|
|
225
|
+
# inner sits at READONLY until its redirects lift it to LOCAL.
|
|
226
|
+
inner = apply_inner_redirects(inner)
|
|
227
|
+
updated_sub = inner.inner_commands.map { |sub| check_inner_system_paths(sub) }
|
|
228
|
+
inner = inner.with(inner_commands: updated_sub) if updated_sub != inner.inner_commands
|
|
229
|
+
|
|
230
|
+
if inner.inner_commands.any? { |sub| sub.classification == Classification::DANGEROUS }
|
|
231
|
+
return inner.with(classification: Classification::DANGEROUS, risk: Risk::HIGH,
|
|
232
|
+
classification_reason: append_reason(inner.classification_reason,
|
|
233
|
+
"elevated to DANGEROUS by inner command"))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
return inner if Classification.severity(inner.classification) < Classification.severity(Classification::LOCAL_EFFECTS)
|
|
237
|
+
|
|
238
|
+
sys_path = inner.argv.find { |t| system_path?(t) }
|
|
239
|
+
if !sys_path && (inner.classification != Classification::DANGEROUS)
|
|
240
|
+
# Redirect targets are attack surface too — the parser moved them
|
|
241
|
+
# out of argv. Skipped when already DANGEROUS so a /dev/tcp
|
|
242
|
+
# redirect does not stack a second reason (outer parity, line ~147).
|
|
243
|
+
sys_path = inner_redirects(inner).map(&:target).find { |t| system_path?(t) }
|
|
244
|
+
end
|
|
245
|
+
if sys_path
|
|
246
|
+
inner.with(classification: Classification::DANGEROUS, risk: Risk::HIGH,
|
|
247
|
+
classification_reason: append_reason(inner.classification_reason,
|
|
248
|
+
"elevated to DANGEROUS: system path #{sys_path}"))
|
|
249
|
+
else
|
|
250
|
+
inner
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Delegated scripts previously lost their redirects at the Matcher
|
|
255
|
+
# boundary (the synthetic re-match was argv-only), so `bash -c
|
|
256
|
+
# 'echo pwned > /etc/shadow'` classified clean while the unwrapped
|
|
257
|
+
# form was blocked. The Matcher now transports redirects as plain
|
|
258
|
+
# data; every bit of redirect semantics stays here — one source of
|
|
259
|
+
# classification truth.
|
|
260
|
+
def apply_inner_redirects(inner)
|
|
261
|
+
redirects = inner_redirects(inner)
|
|
262
|
+
return inner if redirects.empty?
|
|
263
|
+
|
|
264
|
+
write_paths, _read_paths, all_temp = extract_paths(redirects)
|
|
265
|
+
apply_redirects(inner, redirects, write_paths, all_temp)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Nested layers may be plain CommandResults (no redirects field).
|
|
269
|
+
def inner_redirects(inner)
|
|
270
|
+
inner.respond_to?(:redirects) ? Array(inner.redirects) : []
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Inner elevations previously overwrote nothing (the reason field did
|
|
274
|
+
# not exist on InnerCommandResult); when one now fires on a result
|
|
275
|
+
# that already carries a reason, keep both, outer-elevation style.
|
|
276
|
+
def append_reason(prefix, suffix)
|
|
277
|
+
prefix.to_s.empty? ? suffix : "#{prefix}; #{suffix}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def build_expression_result(expression, command_results, all_redirects, parse_warnings)
|
|
281
|
+
directories = collect_directories(command_results)
|
|
282
|
+
all_writes = (command_results.flat_map(&:write_paths).compact +
|
|
283
|
+
collect_inner_write_paths(command_results)).uniq
|
|
284
|
+
all_reads = command_results.flat_map(&:read_paths).compact
|
|
285
|
+
|
|
286
|
+
if command_results.empty? && parse_warnings.any?
|
|
287
|
+
overall = Classification::UNKNOWN
|
|
288
|
+
overall_risk = Risk::HIGH
|
|
289
|
+
elsif command_results.any?
|
|
290
|
+
overall = Classification.max_severity(command_results.map(&:classification))
|
|
291
|
+
overall_risk = Risk.max_severity(command_results.map(&:risk))
|
|
292
|
+
else
|
|
293
|
+
overall = Classification::READONLY
|
|
294
|
+
overall_risk = Risk::LOW
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
expression_reason = nil
|
|
298
|
+
if overall != Classification::DANGEROUS && directories.any? { |d| system_path?(d) } && mutating_pipeline?(command_results)
|
|
299
|
+
overall = Classification::DANGEROUS
|
|
300
|
+
overall_risk = Risk::HIGH
|
|
301
|
+
expression_reason = expression_danger_reason(command_results, directories)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
ExpressionResult.new(
|
|
305
|
+
expression: expression, classification: overall, risk: overall_risk,
|
|
306
|
+
directories: directories, write_paths: all_writes, read_paths: all_reads,
|
|
307
|
+
commands: command_results, redirects: all_redirects, parse_warnings: parse_warnings,
|
|
308
|
+
classification_reason: expression_reason
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Expression-level elevation needs evidence too: without a composed
|
|
313
|
+
# reason the guard falls back to "dangerous_ast" and guidance blames
|
|
314
|
+
# an unnamed pattern instead of the pipeline that trips the gate.
|
|
315
|
+
def expression_danger_reason(command_results, directories)
|
|
316
|
+
mutators = command_results.select { |r| mutator?(r) }
|
|
317
|
+
.map { |r| r.command.join(" ") }.uniq
|
|
318
|
+
named = mutators.first(EXPRESSION_MUTATOR_CAP)
|
|
319
|
+
named << "..." if mutators.size > EXPRESSION_MUTATOR_CAP
|
|
320
|
+
named = "a mutating command" if named.empty?
|
|
321
|
+
"mutating pipeline (#{named.join(', ')}) touches system directory #{directories.find { |d| system_path?(d) }}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def mutator?(result)
|
|
325
|
+
Classification.severity(result.classification) >= Classification.severity(Classification::LOCAL_EFFECTS) ||
|
|
326
|
+
result.inner_commands.any? { |i| Classification.severity(i.classification) >= Classification.severity(Classification::LOCAL_EFFECTS) }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def mutating_pipeline?(command_results)
|
|
330
|
+
command_results.any? { |r| mutator?(r) }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def temp_path?(path)
|
|
334
|
+
p = path.to_s
|
|
335
|
+
p == "/tmp" || p.start_with?("/tmp/") || p == "/var/tmp" || p.start_with?("/var/tmp/") ||
|
|
336
|
+
p.include?("AppData/Local/Temp") || p.include?("AppData\\Local\\Temp")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def real_file_path?(path)
|
|
340
|
+
p = path.to_s
|
|
341
|
+
DEV_PATHS.none? { |dev| p == dev || p.start_with?(dev) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def system_path?(path)
|
|
345
|
+
p = path.to_s.strip
|
|
346
|
+
return false if p.empty? || p.start_with?("-")
|
|
347
|
+
|
|
348
|
+
p = p[1..-2] if (p.start_with?('"') && p.end_with?('"')) || (p.start_with?("'") && p.end_with?("'"))
|
|
349
|
+
return false if p.empty? || p.start_with?("-")
|
|
350
|
+
return false if !p.include?("..") && temp_path?(p)
|
|
351
|
+
|
|
352
|
+
return true if p =~ %r{\A/(?:\*|\.+)?/?\z}
|
|
353
|
+
return true if p =~ %r{\A[a-zA-Z]:(?:[\\/]|\*|\.+)?\z}i
|
|
354
|
+
return true if ["~", "~/", "$HOME", "${HOME}"].include?(p)
|
|
355
|
+
|
|
356
|
+
if p =~ %r{\A([a-zA-Z]):[/\\](.*)\z}
|
|
357
|
+
parts = []
|
|
358
|
+
Regexp.last_match(2).tr("\\", "/").split("/").each { |part| part == ".." ? parts.pop : (parts << part unless [".", ""].include?(part)) }
|
|
359
|
+
win_norm = "#{Regexp.last_match(1).downcase}:/#{parts.join('/')}".downcase
|
|
360
|
+
return false if win_norm.include?("/appdata/local/temp/") || win_norm.end_with?("/appdata/local/temp")
|
|
361
|
+
|
|
362
|
+
return true if win_norm =~ %r{\A[a-z]:/(?:windows|program files|program files \(x86\)|system32)(?:/|\z)} ||
|
|
363
|
+
win_norm =~ %r{\A[a-z]:/users/?(?:\*|\.+)?/?\z} ||
|
|
364
|
+
win_norm =~ %r{\A[a-z]:/?(?:\*|\.+)?/?\z}
|
|
365
|
+
|
|
366
|
+
return false
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
expanded = safe_expand_path(p)
|
|
370
|
+
norm = Rubik::Platform.normalize_path(expanded)
|
|
371
|
+
|
|
372
|
+
return true if Rubik::Platform.dangerous_system_roots.include?(norm) || Rubik::Platform.drive_root?(expanded)
|
|
373
|
+
|
|
374
|
+
if norm.start_with?("/tmp/") || norm.start_with?("/var/tmp/") ||
|
|
375
|
+
norm.include?("/appdata/local/temp/") || norm.include?("\\appdata\\local\\temp\\")
|
|
376
|
+
return false
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
return true if SYSTEM_DIRS.any? { |sys| Rubik::Platform.path_under?(norm, sys) }
|
|
380
|
+
|
|
381
|
+
false
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def safe_expand_path(path)
|
|
385
|
+
File.expand_path(path)
|
|
386
|
+
rescue ArgumentError, TypeError
|
|
387
|
+
# NUL byte / bad tilde / invalid encoding raise ArgumentError,
|
|
388
|
+
# non-String without to_str raises TypeError — fall back to the
|
|
389
|
+
# raw token.
|
|
390
|
+
path
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def collect_directories(results)
|
|
394
|
+
dirs = []
|
|
395
|
+
results.each do |r|
|
|
396
|
+
dirs << r.argv[1] if r.command.any? && ["cd", "pushd"].include?(r.command[0]) && r.argv.size > 1
|
|
397
|
+
dirs.concat(r.directories) if r.directories
|
|
398
|
+
dirs.concat(extract_argv_directories(r.command, r.argv))
|
|
399
|
+
dirs.concat(collect_inner_directories(r.inner_commands))
|
|
400
|
+
end
|
|
401
|
+
dirs.compact.uniq
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def collect_inner_directories(inners)
|
|
405
|
+
dirs = []
|
|
406
|
+
inners.each do |inner|
|
|
407
|
+
dirs.concat(extract_argv_directories(inner.command, inner.argv))
|
|
408
|
+
dirs.concat(collect_inner_directories(inner.inner_commands))
|
|
409
|
+
end
|
|
410
|
+
dirs
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Inner redirects were invisible to the top-level aggregation; their
|
|
414
|
+
# write targets bucket with the same WRITE_OPERATORS semantics as
|
|
415
|
+
# top-level ones.
|
|
416
|
+
def collect_inner_write_paths(results)
|
|
417
|
+
results.flat_map do |r|
|
|
418
|
+
writes, = extract_paths(inner_redirects(r))
|
|
419
|
+
writes + collect_inner_write_paths(Array(r.inner_commands))
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def extract_argv_directories(command, argv)
|
|
424
|
+
return [] if command.empty? || argv.size <= 1
|
|
425
|
+
|
|
426
|
+
bin = command[0]
|
|
427
|
+
pos = case bin
|
|
428
|
+
when "find", "ls" then argv[1..].find { |a| !a.start_with?("-") }
|
|
429
|
+
when "cat", "head", "tail", "less", "more"
|
|
430
|
+
t = argv[1..].find { |a| !a.start_with?("-") && a.include?("/") }
|
|
431
|
+
File.dirname(t) if t
|
|
432
|
+
when "echo", "printf"
|
|
433
|
+
argv[1..].find { |a| !a.start_with?("-") && (system_path?(a) || a.include?("/")) }
|
|
434
|
+
end
|
|
435
|
+
pos ? [pos] : []
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: agent-browser
|
|
3
|
+
description: "Fast browser automation CLI for AI agents"
|
|
4
|
+
classification: EXTERNAL_EFFECTS
|
|
5
|
+
strict: false
|
|
6
|
+
global_options:
|
|
7
|
+
--help: {overrides: READONLY}
|
|
8
|
+
--version: {overrides: READONLY}
|
|
9
|
+
-V: {overrides: READONLY}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: apt
|
|
3
|
+
description: "Debian package management tool"
|
|
4
|
+
strict: false
|
|
5
|
+
global_options:
|
|
6
|
+
--help: {overrides: READONLY}
|
|
7
|
+
subcommands:
|
|
8
|
+
list:
|
|
9
|
+
classification: READONLY
|
|
10
|
+
show:
|
|
11
|
+
classification: READONLY
|
|
12
|
+
search:
|
|
13
|
+
classification: READONLY
|
|
14
|
+
install:
|
|
15
|
+
classification: DANGEROUS
|
|
16
|
+
remove:
|
|
17
|
+
classification: DANGEROUS
|
|
18
|
+
purge:
|
|
19
|
+
classification: DANGEROUS
|
|
20
|
+
update:
|
|
21
|
+
classification: EXTERNAL_EFFECTS
|
|
22
|
+
upgrade:
|
|
23
|
+
classification: DANGEROUS
|
|
24
|
+
autoremove:
|
|
25
|
+
classification: DANGEROUS
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: awk
|
|
3
|
+
description: "Pattern scanning and text processing language"
|
|
4
|
+
classification: READONLY
|
|
5
|
+
strict: false
|
|
6
|
+
global_options:
|
|
7
|
+
--help: {overrides: READONLY}
|
|
8
|
+
options:
|
|
9
|
+
-i: {overrides: DANGEROUS, risk: HIGH}
|
|
10
|
+
--inplace: {overrides: DANGEROUS, risk: HIGH, aliases: [-i]}
|