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,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
require_relative "../security/errors"
|
|
9
|
+
|
|
10
|
+
module Rubik
|
|
11
|
+
module Tools
|
|
12
|
+
# Runs synthesized handler code in a short-lived child ruby process with a
|
|
13
|
+
# hard kill deadline (AGENTS.md §3.4 Session Isolation).
|
|
14
|
+
#
|
|
15
|
+
# Why a subprocess: Timeout.timeout relies on Thread.raise, which cannot
|
|
16
|
+
# preempt C extensions or uninterruptible blocking syscalls — a runaway
|
|
17
|
+
# handler could hang the whole REPL past its budget. A child process can
|
|
18
|
+
# always be killed. When spawning is impossible (exotic embeddings, strict
|
|
19
|
+
# sandboxes) the runner reports :spawn_failed so callers can fall back to
|
|
20
|
+
# the in-process Timeout path.
|
|
21
|
+
module PreflightRunner
|
|
22
|
+
DEFAULT_TIMEOUT = 5
|
|
23
|
+
|
|
24
|
+
# Child evaluator: reads {code:, args:, mode:} from stdin, calls the
|
|
25
|
+
# handler, prints a single JSON verdict line. Single-quoted heredoc —
|
|
26
|
+
# never interpolated into the parent's source.
|
|
27
|
+
CHILD_SCRIPT = <<~'RUBY'
|
|
28
|
+
require "json"
|
|
29
|
+
require "stringio"
|
|
30
|
+
begin
|
|
31
|
+
payload = JSON.parse($stdin.read)
|
|
32
|
+
handler = eval(payload["code"], Object.new.instance_eval { binding }, __FILE__, __LINE__)
|
|
33
|
+
raise "code did not evaluate to a callable Proc" unless handler.respond_to?(:call)
|
|
34
|
+
# 2-arity command handlers receive the live CommandContext at
|
|
35
|
+
# dispatch; preflight mirrors its OUTPUT VIEW (out/puts/print/
|
|
36
|
+
# flush delegate to a throwaway StringIO) without the repl/
|
|
37
|
+
# require. Service reads fail loudly here — such handlers should
|
|
38
|
+
# declare 3-arity and be verified with the raw shell instead.
|
|
39
|
+
command_view = Struct.new(:out) do
|
|
40
|
+
def puts(*args)
|
|
41
|
+
out.puts(*args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def print(*args)
|
|
45
|
+
out.print(*args)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def flush
|
|
49
|
+
out.flush if out.respond_to?(:flush)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
case payload["mode"]
|
|
53
|
+
when "tool"
|
|
54
|
+
handler.call(payload["args"])
|
|
55
|
+
else
|
|
56
|
+
view = command_view.new(StringIO.new)
|
|
57
|
+
case handler.arity
|
|
58
|
+
when 1 then handler.call(payload["args"])
|
|
59
|
+
when 2 then handler.call(payload["args"], view)
|
|
60
|
+
else handler.call(payload["args"], view.out, nil)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
STDOUT.puts JSON.dump(ok: true)
|
|
64
|
+
rescue StandardError, ScriptError => e
|
|
65
|
+
STDOUT.puts JSON.dump(ok: false, error: "#{e.class}: #{e.message}")
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
|
|
69
|
+
module_function
|
|
70
|
+
|
|
71
|
+
# Executes +code+ (a self-contained lambda) with +args+ in an isolated
|
|
72
|
+
# child process. mode: :tool mirrors the ToolWrapper contract (exactly
|
|
73
|
+
# one argument); :command mirrors the REPL arity dispatch (args, out,
|
|
74
|
+
# nil shell — the real shell is unavailable in the child).
|
|
75
|
+
#
|
|
76
|
+
# The child process has no Rubik loaded, so payload["code"] cannot be
|
|
77
|
+
# verified there — the ONLY verification it can rely on is whatever
|
|
78
|
+
# the parent proved BEFORE spawning. guard is a REQUIRED kwarg on
|
|
79
|
+
# purpose: a future caller cannot skip the spawn-boundary check
|
|
80
|
+
# silently (ArgumentError at the call site).
|
|
81
|
+
#
|
|
82
|
+
# Returns nil on success, an error-detail String on failure, or
|
|
83
|
+
# :spawn_failed when the child infrastructure is unusable.
|
|
84
|
+
def run(code, args, guard:, mode: :command, timeout: DEFAULT_TIMEOUT)
|
|
85
|
+
# Shared gate at the spawn boundary: the duck-typed downgrade (a
|
|
86
|
+
# test-double affordance — production guards always define
|
|
87
|
+
# verify_synthesis_safety!) warns loudly at the single seam instead
|
|
88
|
+
# of silently skipping the AST deny surface here too.
|
|
89
|
+
Rubik::Security::Guard.run_synthesis_gate(guard, code)
|
|
90
|
+
payload = JSON.dump(code: code, args: args || {}, mode: mode.to_s)
|
|
91
|
+
run_subprocess(payload, timeout)
|
|
92
|
+
rescue Rubik::Security::BlockedWriteError => e
|
|
93
|
+
# Must precede the generic rescue: BlockedWriteError is a
|
|
94
|
+
# StandardError, and a block verdict downgraded to :spawn_failed
|
|
95
|
+
# would make the caller's in-process fallback run the blocked code.
|
|
96
|
+
"security verification failed: #{e.message}"
|
|
97
|
+
rescue TypeError, JSON::GeneratorError, SystemCallError
|
|
98
|
+
# Non-JSON-serializable args (TypeError/GeneratorError) or any other
|
|
99
|
+
# payload problem — caller falls back to in-process.
|
|
100
|
+
:spawn_failed
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# In-process fallback (subprocess unavailable). Yields the caller's
|
|
104
|
+
# verification block under a Timeout guard. Returns nil on success or
|
|
105
|
+
# an error-detail String. Same caveat as before: Timeout cannot preempt
|
|
106
|
+
# C extensions or uninterruptible syscalls.
|
|
107
|
+
def verify_in_process(timeout: DEFAULT_TIMEOUT, &)
|
|
108
|
+
Timeout.timeout(timeout, &)
|
|
109
|
+
nil
|
|
110
|
+
rescue Timeout::Error
|
|
111
|
+
"execution timed out after #{timeout} seconds"
|
|
112
|
+
rescue StandardError, ScriptError => e
|
|
113
|
+
"#{e.class}: #{e.message}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parent-side mirror of the child evaluator's dispatch (see CHILD_SCRIPT).
|
|
117
|
+
# Keeps the fallback path semantically identical to the subprocess path.
|
|
118
|
+
def call_handler(handler, args, mode: :command, shell: nil)
|
|
119
|
+
if mode == :tool
|
|
120
|
+
handler.call(args)
|
|
121
|
+
else
|
|
122
|
+
command_view = Struct.new(:out) do
|
|
123
|
+
def puts(*)
|
|
124
|
+
out.puts(*)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def print(*)
|
|
128
|
+
out.print(*)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def flush
|
|
132
|
+
out.flush if out.respond_to?(:flush)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
view = command_view.new(StringIO.new)
|
|
136
|
+
case handler.arity
|
|
137
|
+
when 1 then handler.call(args)
|
|
138
|
+
when 2 then handler.call(args, view)
|
|
139
|
+
else handler.call(args, view.out, shell)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def run_subprocess(payload, timeout)
|
|
145
|
+
rd_in = wr_in = rd_out = wr_out = nil
|
|
146
|
+
rd_in, wr_in = IO.pipe
|
|
147
|
+
rd_out, wr_out = IO.pipe
|
|
148
|
+
pid = Process.spawn(RbConfig.ruby, "-e", CHILD_SCRIPT, in: rd_in, out: wr_out, err: File::NULL)
|
|
149
|
+
rd_in.close
|
|
150
|
+
wr_out.close
|
|
151
|
+
wr_in.write(payload)
|
|
152
|
+
wr_in.close
|
|
153
|
+
|
|
154
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
155
|
+
output = +""
|
|
156
|
+
loop do
|
|
157
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
158
|
+
if remaining <= 0
|
|
159
|
+
kill_child(pid)
|
|
160
|
+
return "execution timed out after #{timeout} seconds (subprocess killed)"
|
|
161
|
+
end
|
|
162
|
+
# wait_readable returning false (deadline slice elapsed) must NOT
|
|
163
|
+
# break: the deadline check above is what kills a runaway child.
|
|
164
|
+
next unless rd_out.wait_readable(remaining)
|
|
165
|
+
|
|
166
|
+
chunk = rd_out.read_nonblock(4096, exception: false)
|
|
167
|
+
break if chunk.nil? # EOF — child finished
|
|
168
|
+
next if chunk == :wait_readable
|
|
169
|
+
|
|
170
|
+
output << chunk
|
|
171
|
+
end
|
|
172
|
+
begin
|
|
173
|
+
Process.wait(pid)
|
|
174
|
+
rescue StandardError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
parse_verdict(output)
|
|
178
|
+
rescue StandardError
|
|
179
|
+
kill_child(pid) if defined?(pid) && pid
|
|
180
|
+
:spawn_failed
|
|
181
|
+
ensure
|
|
182
|
+
[rd_in, wr_in, rd_out, wr_out].each do |io|
|
|
183
|
+
io.close unless io.nil? || io.closed?
|
|
184
|
+
rescue StandardError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def kill_child(pid)
|
|
190
|
+
Process.kill("KILL", pid)
|
|
191
|
+
Process.wait(pid)
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def parse_verdict(output)
|
|
197
|
+
line = output.lines.reverse.find { |l| l.start_with?("{") }
|
|
198
|
+
return "subprocess produced no verdict" unless line
|
|
199
|
+
|
|
200
|
+
verdict = JSON.parse(line)
|
|
201
|
+
return nil if verdict["ok"]
|
|
202
|
+
|
|
203
|
+
verdict["error"].to_s
|
|
204
|
+
rescue JSON::ParserError
|
|
205
|
+
"subprocess produced unreadable output"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "services"
|
|
6
|
+
require_relative "../coeffects"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
class Registry
|
|
11
|
+
def initialize(coeffect_ctx: nil)
|
|
12
|
+
@tools = {}
|
|
13
|
+
@aliases = {}
|
|
14
|
+
@tool_aliases = {}
|
|
15
|
+
@origins = {}
|
|
16
|
+
@coeffect_ctx = coeffect_ctx
|
|
17
|
+
@monitor = Monitor.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# origin labels where a tool came from (:builtin, :overlay, :mcp,
|
|
21
|
+
# :synthesis). The synthesizer uses it to refuse shadowing built-ins
|
|
22
|
+
# while still allowing hot-updates of non-builtin tools.
|
|
23
|
+
def register(tool_instance, aliases: [], origin: :builtin)
|
|
24
|
+
@monitor.synchronize do
|
|
25
|
+
@tools[tool_instance.name] = tool_instance
|
|
26
|
+
@origins[tool_instance.name] = origin
|
|
27
|
+
all_aliases = (tool_instance.respond_to?(:aliases) ? Array(tool_instance.aliases) : []) + Array(aliases)
|
|
28
|
+
@tool_aliases[tool_instance.name] ||= []
|
|
29
|
+
all_aliases.each do |al|
|
|
30
|
+
clean_al = al.to_s.downcase
|
|
31
|
+
@aliases[clean_al] = tool_instance.name
|
|
32
|
+
@tool_aliases[tool_instance.name] << clean_al
|
|
33
|
+
end
|
|
34
|
+
tool_instance
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unregister(name)
|
|
39
|
+
@monitor.synchronize do
|
|
40
|
+
canonical_name = @tools.key?(name.to_s) ? name.to_s : @aliases[name.to_s.downcase]
|
|
41
|
+
removed_tool = @tools.delete(canonical_name) if canonical_name
|
|
42
|
+
@origins.delete(canonical_name) if canonical_name
|
|
43
|
+
if canonical_name && @tool_aliases[canonical_name]
|
|
44
|
+
@tool_aliases.delete(canonical_name).each do |al|
|
|
45
|
+
@aliases.delete(al) if @aliases[al] == canonical_name
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
@aliases.delete_if { |_k, v| v == canonical_name || v == name.to_s }
|
|
49
|
+
end
|
|
50
|
+
@aliases.delete(name.to_s.downcase) if @aliases[name.to_s.downcase] == canonical_name
|
|
51
|
+
removed_tool
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Atomic hot-swap of an existing tool with a new tool instance.
|
|
56
|
+
# Reentrant lock safety: Monitor#synchronize is explicitly reentrant (recursive),
|
|
57
|
+
# meaning the calling thread can safely enter unregister and register without deadlock.
|
|
58
|
+
def swap(name, new_tool, origin: :builtin)
|
|
59
|
+
@monitor.synchronize do
|
|
60
|
+
unregister(name)
|
|
61
|
+
register(new_tool, origin: origin) if new_tool
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get(tool_name)
|
|
66
|
+
@monitor.synchronize do
|
|
67
|
+
@tools[tool_name.to_s] || begin
|
|
68
|
+
canonical = @aliases[tool_name.to_s.downcase]
|
|
69
|
+
canonical ? @tools[canonical] : nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Registration origin of the tool resolved by name (canonical or alias),
|
|
75
|
+
# or nil when no such tool is registered.
|
|
76
|
+
def origin(name)
|
|
77
|
+
@monitor.synchronize do
|
|
78
|
+
canonical = @tools.key?(name.to_s) ? name.to_s : @aliases[name.to_s.downcase]
|
|
79
|
+
canonical ? @origins[canonical] : nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def list
|
|
84
|
+
@monitor.synchronize do
|
|
85
|
+
@tools.values.dup
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Schemas of tools whose declared coeffects (REQUIRES) are currently
|
|
90
|
+
# satisfied — the toolbelt-side of the fiber state machine (AGENTS.md
|
|
91
|
+
# §2): a tool missing its coeffects sits PENDING (hidden from the LLM)
|
|
92
|
+
# and activates reactively when a provider supplies the service.
|
|
93
|
+
# Called per agent step, so the filter re-evaluates live. Tools remain
|
|
94
|
+
# invocable via Registry#get for graceful self-correction errors.
|
|
95
|
+
def schemas
|
|
96
|
+
@monitor.synchronize do
|
|
97
|
+
@tools.values.select { |t| coeffects_satisfied?(t) }.map(&:to_h)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def aliases_map
|
|
102
|
+
@monitor.synchronize do
|
|
103
|
+
@aliases.dup
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Service slots (guard, context_index, adapter_registry, ast_graph,
|
|
108
|
+
# task_manager, sandbox_runner, subagent_supervisor) resolve through
|
|
109
|
+
# the house triple chain in Tools::Services — explicit kwarg (the
|
|
110
|
+
# **service_overrides above), then the runtime coeffect, then a fresh
|
|
111
|
+
# standalone default — so standalone/test constructions stay
|
|
112
|
+
# fail-closed. Unknown service kwargs raise ArgumentError, naming the
|
|
113
|
+
# valid slots.
|
|
114
|
+
def self.build_default(
|
|
115
|
+
buffer_manager:, shell: nil, overlay_storage: nil, runtime_manager: nil,
|
|
116
|
+
lsp_manager: nil, config: nil, root_dir: Dir.pwd, **service_overrides
|
|
117
|
+
)
|
|
118
|
+
ctx = runtime_manager&.context
|
|
119
|
+
registry = new(coeffect_ctx: ctx.respond_to?(:satisfied?) ? ctx : nil)
|
|
120
|
+
native_switch = config || shell&.config
|
|
121
|
+
svc = Services.new(ctx: ctx, overrides: service_overrides, root_dir: root_dir, native_switch: native_switch)
|
|
122
|
+
# Raw kwarg, NOT svc.context_index — preserved verbatim from the
|
|
123
|
+
# pre-catalog registry: RunTestsTool takes the caller-supplied (and
|
|
124
|
+
# possibly nil) index while list/grep tools take the resolved one.
|
|
125
|
+
raw_context_index = service_overrides[:context_index]
|
|
126
|
+
|
|
127
|
+
register_file_and_search_tools(registry, buffer_manager: buffer_manager, svc: svc, root_dir: root_dir, native_switch: native_switch, ctx: ctx)
|
|
128
|
+
register_lsp_tools(registry, lsp_manager: lsp_manager, buffer_manager: buffer_manager)
|
|
129
|
+
register_execution_tools(registry, svc: svc, raw_context_index: raw_context_index, root_dir: root_dir, native_switch: native_switch, ctx: ctx)
|
|
130
|
+
register_repl_and_synthesis_tools(
|
|
131
|
+
registry,
|
|
132
|
+
shell: shell,
|
|
133
|
+
overlay_storage: overlay_storage,
|
|
134
|
+
runtime_manager: runtime_manager,
|
|
135
|
+
lsp_manager: lsp_manager,
|
|
136
|
+
svc: svc,
|
|
137
|
+
native_switch: native_switch,
|
|
138
|
+
root_dir: root_dir
|
|
139
|
+
)
|
|
140
|
+
register_buffer_and_ui_tools(registry, buffer_manager: buffer_manager, shell: shell, ctx: ctx)
|
|
141
|
+
register_subagent_tools(registry, svc: svc, ctx: ctx)
|
|
142
|
+
register_system_tools(registry, svc: svc, native_switch: native_switch, root_dir: root_dir, ctx: ctx)
|
|
143
|
+
|
|
144
|
+
overlay_storage&.load_tools(registry)
|
|
145
|
+
# Inverse-by-construction (AGENTS.md §1.A): one monoidal inverse for
|
|
146
|
+
# the whole registry — context teardown unregisters every tool in
|
|
147
|
+
# strict LIFO order together with the rest of the session effects.
|
|
148
|
+
ctx&.register_disposable { registry.list.each { |t| registry.unregister(t.name) } } if ctx.respond_to?(:register_disposable)
|
|
149
|
+
registry
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.register_file_and_search_tools(registry, buffer_manager:, svc:, root_dir:, native_switch:, ctx:)
|
|
153
|
+
registry.register(ReadFileTool.new(buffer_manager, runtime_context: ctx))
|
|
154
|
+
registry.register(ReplaceFileContentTool.new(buffer_manager))
|
|
155
|
+
registry.register(WriteFileTool.new(buffer_manager))
|
|
156
|
+
registry.register(ListFilesTool.new(svc.context_index, root_dir: root_dir, runtime_context: ctx))
|
|
157
|
+
registry.register(GrepSearchTool.new(buffer_manager, svc.context_index, root_dir: root_dir, runtime_context: ctx, config: native_switch))
|
|
158
|
+
registry.register(SearchSymbolsTool.new(svc.ast_graph, runtime_context: ctx))
|
|
159
|
+
registry.register(GetOutlineTool.new(svc.ast_graph, runtime_context: ctx))
|
|
160
|
+
registry.register(FindReferencesTool.new(svc.ast_graph, runtime_context: ctx))
|
|
161
|
+
registry.register(GetSymbolTool.new(svc.ast_graph, runtime_context: ctx))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.register_lsp_tools(registry, lsp_manager:, buffer_manager:)
|
|
165
|
+
registry.register(LSPDiagnosticsTool.new(lsp_manager, buffer_manager: buffer_manager))
|
|
166
|
+
registry.register(LSPHoverTool.new(lsp_manager, buffer_manager: buffer_manager))
|
|
167
|
+
registry.register(LSPDefinitionTool.new(lsp_manager, buffer_manager: buffer_manager))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.register_execution_tools(registry, svc:, raw_context_index:, root_dir:, native_switch:, ctx:)
|
|
171
|
+
registry.register(RunTestsTool.new(svc.adapter_registry, raw_context_index, root_dir: root_dir,
|
|
172
|
+
config: native_switch, guard: svc.guard))
|
|
173
|
+
registry.register(RunCommandTool.new(
|
|
174
|
+
guard: svc.guard,
|
|
175
|
+
runner: svc.sandbox_runner,
|
|
176
|
+
root_dir: root_dir,
|
|
177
|
+
task_manager: svc.task_manager,
|
|
178
|
+
runtime_context: ctx
|
|
179
|
+
))
|
|
180
|
+
registry.register(ManageTaskTool.new(task_manager: svc.task_manager, guard: svc.guard, runtime_context: ctx))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.register_repl_and_synthesis_tools(registry, shell:, overlay_storage:, runtime_manager:, lsp_manager:, svc:, native_switch:, root_dir:)
|
|
184
|
+
registry.register(ExecReplCommandTool.new(shell))
|
|
185
|
+
registry.register(SynthesizeCommandTool.new(shell, overlay_storage, svc.guard, runtime_manager: runtime_manager))
|
|
186
|
+
registry.register(SynthesizeToolTool.new(registry, overlay_storage, svc.guard, runtime_manager: runtime_manager))
|
|
187
|
+
registry.register(UnsynthesizeTool.new(shell, registry, overlay_storage, runtime_manager: runtime_manager))
|
|
188
|
+
registry.register(InspectRuntimeTool.new(runtime_manager, registry, shell))
|
|
189
|
+
registry.register(ProvideServiceTool.new(runtime_manager))
|
|
190
|
+
registry.register(RepairRuntimeTool.new(
|
|
191
|
+
runtime_manager: runtime_manager,
|
|
192
|
+
lsp_manager: lsp_manager,
|
|
193
|
+
adapter_registry: svc.adapter_registry,
|
|
194
|
+
overlay_storage: overlay_storage,
|
|
195
|
+
config: native_switch,
|
|
196
|
+
root_dir: root_dir,
|
|
197
|
+
guard: svc.guard
|
|
198
|
+
))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.register_buffer_and_ui_tools(registry, buffer_manager:, shell:, ctx:)
|
|
202
|
+
registry.register(ApplyBufferTool.new(buffer_manager, shell))
|
|
203
|
+
registry.register(AskQuestionTool.new(shell: shell, runtime_context: ctx))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.register_subagent_tools(registry, svc:, ctx:)
|
|
207
|
+
registry.register(InvokeSubagentTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
|
|
208
|
+
registry.register(ManageSubagentsTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
|
|
209
|
+
registry.register(SendMessageTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
|
|
210
|
+
registry.register(DefineSubagentTool.new(supervisor: svc.subagent_supervisor, runtime_context: ctx))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.register_system_tools(registry, svc:, native_switch:, root_dir:, ctx:)
|
|
214
|
+
registry.register(DmesgTool.new(svc.dmesg, runtime_context: ctx))
|
|
215
|
+
registry.register(StorageTool.new(nil, runtime_context: ctx, config: native_switch, root_dir: root_dir))
|
|
216
|
+
registry.register(VakanuvisTool.new(runtime_context: ctx))
|
|
217
|
+
registry.register(PruneContextTool.new(runtime_context: ctx))
|
|
218
|
+
registry.register(UpdateGoalLedgerTool.new(runtime_context: ctx))
|
|
219
|
+
registry.register(ShowFileTool.new(runtime_context: ctx))
|
|
220
|
+
registry.register(FinishTool.new)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private_class_method :register_file_and_search_tools,
|
|
224
|
+
:register_lsp_tools,
|
|
225
|
+
:register_execution_tools,
|
|
226
|
+
:register_repl_and_synthesis_tools,
|
|
227
|
+
:register_buffer_and_ui_tools,
|
|
228
|
+
:register_subagent_tools,
|
|
229
|
+
:register_system_tools
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
# A tool is satisfied when it declares no REQUIRES, when no coeffect
|
|
234
|
+
# context is wired (plain registries keep advertising everything), or
|
|
235
|
+
# when the context chain satisfies every declared dependency.
|
|
236
|
+
def coeffects_satisfied?(tool)
|
|
237
|
+
return true unless tool.class.const_defined?(:REQUIRES)
|
|
238
|
+
|
|
239
|
+
deps = tool.class.const_get(:REQUIRES)
|
|
240
|
+
return true if deps.nil? || deps.empty?
|
|
241
|
+
return true unless @coeffect_ctx.respond_to?(:satisfied?)
|
|
242
|
+
|
|
243
|
+
@coeffect_ctx.satisfied?(deps)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../platform"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Tools
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :tool_name, :call_id, :status, :output, :error, :exit_code, :duration_ms, :truncated, :metadata
|
|
9
|
+
|
|
10
|
+
def initialize(tool_name:, call_id: nil, status: :ok, output: "", error: nil, exit_code: nil, duration_ms: 0.0, truncated: false, metadata: {})
|
|
11
|
+
@tool_name = tool_name.to_s
|
|
12
|
+
@call_id = call_id.to_s
|
|
13
|
+
@status = status.to_sym
|
|
14
|
+
@output = output.to_s
|
|
15
|
+
@error = error
|
|
16
|
+
@exit_code = exit_code
|
|
17
|
+
@duration_ms = duration_ms.to_f.round(1)
|
|
18
|
+
@truncated = truncated ? true : false
|
|
19
|
+
@metadata = metadata || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ok?
|
|
23
|
+
@status == :ok
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error?
|
|
27
|
+
@status == :error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the tool result output as a plain string.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] the output content of this tool result
|
|
33
|
+
def to_s
|
|
34
|
+
@output
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# XML envelope for the conversation history. Both attributes and the
|
|
38
|
+
# output body are escaped: tool output is untrusted content (file
|
|
39
|
+
# contents, command output) and an unescaped "</tool_result>" or XML
|
|
40
|
+
# tag inside it could break the envelope or smuggle instructions into
|
|
41
|
+
# the prompt.
|
|
42
|
+
def to_xml
|
|
43
|
+
esc = Rubik::Platform.method(:xml_escape)
|
|
44
|
+
attrs = [
|
|
45
|
+
%(tool="#{esc.call(@tool_name)}"),
|
|
46
|
+
@call_id && !@call_id.empty? ? %(id="#{esc.call(@call_id)}") : nil,
|
|
47
|
+
%(status="#{@status}"),
|
|
48
|
+
@exit_code ? %(exit_code="#{@exit_code}") : nil,
|
|
49
|
+
%(duration_ms="#{@duration_ms}"),
|
|
50
|
+
@truncated ? %(truncated="true") : nil
|
|
51
|
+
].compact.join(" ")
|
|
52
|
+
|
|
53
|
+
"<tool_result #{attrs}>\n#{esc.call(@output)}\n</tool_result>"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
tool: @tool_name,
|
|
59
|
+
call_id: @call_id,
|
|
60
|
+
status: @status,
|
|
61
|
+
output: @output,
|
|
62
|
+
error: @error,
|
|
63
|
+
exit_code: @exit_code,
|
|
64
|
+
duration_ms: @duration_ms,
|
|
65
|
+
truncated: @truncated,
|
|
66
|
+
metadata: @metadata
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../coeffects"
|
|
4
|
+
require_relative "../kernel/dmesg" unless defined?(Rubik::Kernel::Dmesg)
|
|
5
|
+
|
|
6
|
+
module Rubik
|
|
7
|
+
module Tools
|
|
8
|
+
# Lazy per-key resolution view for the tool layer (architecture-review
|
|
9
|
+
# Candidate 05): ONE home for the house triple chain — explicit override
|
|
10
|
+
# kwarg, then the runtime coeffect (Coeffects.get), then a fresh
|
|
11
|
+
# standalone default. Collapses Registry.build_default's hand-written
|
|
12
|
+
# effective_* locals into a tier table.
|
|
13
|
+
#
|
|
14
|
+
# owns : the resolution tiers per service and the fresh-default
|
|
15
|
+
# table — standalone construction policy: a zero-policy Guard
|
|
16
|
+
# and a guard-chained sandbox Runner keep toolbelt-only
|
|
17
|
+
# builds fail-closed (never wider policy than the caller gave)
|
|
18
|
+
# leaves : key STRINGS (Rubik::Coeffects SSOT); full-graph construction
|
|
19
|
+
# (REPL::ServiceCatalog — the fresh tier is deliberately the
|
|
20
|
+
# DEGENERATE construction, not Composition's); tool
|
|
21
|
+
# constructor signatures (tools layer)
|
|
22
|
+
#
|
|
23
|
+
# The coeffect tier exists exactly where wire_runtime_context publishes
|
|
24
|
+
# the key (guard, context_index, adapter_registry, ast_graph,
|
|
25
|
+
# task_manager, sandbox, subagent supervisor), so a shell-built registry
|
|
26
|
+
# always resolves the session's own instances. ORDER is load-bearing:
|
|
27
|
+
# sandbox_runner's fresh default chains onto the RESOLVED guard, so
|
|
28
|
+
# :guard resolves first.
|
|
29
|
+
class Services
|
|
30
|
+
# Hash literal order IS resolution order: :guard resolves first because
|
|
31
|
+
# sandbox_runner's fresh default chains onto the RESOLVED guard. ORDER
|
|
32
|
+
# derives from the keys so the reader methods and the validation
|
|
33
|
+
# message can never drift from the table.
|
|
34
|
+
COEFFECTS = {
|
|
35
|
+
guard: Rubik::Coeffects::GUARD,
|
|
36
|
+
context_index: Rubik::Coeffects::CONTEXT_INDEX,
|
|
37
|
+
adapter_registry: Rubik::Coeffects::ADAPTER_REGISTRY,
|
|
38
|
+
ast_graph: Rubik::Coeffects::AST_GRAPH,
|
|
39
|
+
task_manager: Rubik::Coeffects::TASK_MANAGER,
|
|
40
|
+
sandbox_runner: Rubik::Coeffects::SANDBOX_RUNNER,
|
|
41
|
+
subagent_supervisor: Rubik::Coeffects::SUBAGENT_SUPERVISOR,
|
|
42
|
+
dmesg: Rubik::Coeffects::DMESG
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
ORDER = COEFFECTS.keys.freeze
|
|
46
|
+
|
|
47
|
+
FRESH = {
|
|
48
|
+
guard: ->(_s) { Rubik::Security::Guard.new },
|
|
49
|
+
context_index: ->(s) { Rubik::Context::Index.new(root_dir: s.root_dir, native: s.native_switch&.fetch("tools.native")) },
|
|
50
|
+
adapter_registry: ->(_s) { Rubik::Adapter::Registry.new },
|
|
51
|
+
ast_graph: ->(_s) { Rubik::AST::Graph.new },
|
|
52
|
+
sandbox_runner: ->(s) { Rubik::Sandbox::Runner.new(root_dir: s.root_dir, guard: s.resolved_guard) },
|
|
53
|
+
dmesg: ->(_s) { Rubik::Kernel::Dmesg.new }
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
attr_reader :root_dir, :native_switch
|
|
57
|
+
|
|
58
|
+
def initialize(ctx: nil, overrides: {}, root_dir: Dir.pwd, native_switch: nil)
|
|
59
|
+
unknown = overrides.keys - ORDER
|
|
60
|
+
raise ArgumentError, "unknown service override(s) #{unknown.inspect}; valid slots: #{ORDER.inspect}" unless unknown.empty?
|
|
61
|
+
|
|
62
|
+
@ctx = ctx
|
|
63
|
+
@overrides = overrides
|
|
64
|
+
@root_dir = root_dir
|
|
65
|
+
@native_switch = native_switch
|
|
66
|
+
@resolved = {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
ORDER.each do |slot|
|
|
70
|
+
define_method(slot) { resolve(slot) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The guard the sandbox fresh default chains onto (resolved first by
|
|
74
|
+
# ORDER). Public for the FRESH lambdas and for the tier tests.
|
|
75
|
+
def resolved_guard
|
|
76
|
+
resolve(:guard)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# The triple chain, memoized per slot: explicit override, then the
|
|
82
|
+
# runtime coeffect (canonical key first, legacy alias second via
|
|
83
|
+
# Coeffects.get), then the fresh standalone default. Slots without a
|
|
84
|
+
# tier (task_manager, subagent_supervisor) resolve to nil when neither
|
|
85
|
+
# the override nor the context supplies one — matching the registry's
|
|
86
|
+
# pre-catalog behavior.
|
|
87
|
+
def resolve(name)
|
|
88
|
+
return @resolved[name] if @resolved.key?(name)
|
|
89
|
+
|
|
90
|
+
@resolved[name] = @overrides[name] ||
|
|
91
|
+
(@ctx && Rubik::Coeffects.get(@ctx, COEFFECTS.fetch(name))) ||
|
|
92
|
+
FRESH[name]&.call(self)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|