Commit 9a2cb59
Changed files (10)
lib
elelem
spec
elelem
tools
lib/elelem/tools/confirm.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Elelem::Plugins.register(:confirm) do |agent|
+ permissions = Elelem::Tools::Permissions.new
+
+ agent.toolbox.before do |args, tool_name:|
+ permissions.check(tool_name, args, terminal: agent.terminal)
+ end
+end
lib/elelem/tools/permissions.json
@@ -0,0 +1,5 @@
+{
+ "execute": "ask",
+ "read": "allow",
+ "write": "allow"
+}
lib/elelem/tools/permissions.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Elelem
+ module Tools
+ class Permissions
+ LOAD_PATHS = [
+ File.expand_path("permissions.json", __dir__),
+ "~/.agents/permissions.json",
+ ".agents/permissions.json"
+ ].freeze
+
+ def initialize(rules: default_rules)
+ @rules = rules
+ end
+
+ def check(tool_name, args, terminal:)
+ policy = @rules[tool_name.to_sym] || :ask
+ case policy
+ when :allow then true
+ when :deny then raise "Permission denied: #{tool_name}"
+ when :ask then prompt(tool_name, args, terminal)
+ end
+ end
+
+ private
+
+ def load_config(path)
+ return {} unless File.exist?(path)
+
+ JSON.parse(File.read(path)).transform_keys(&:to_sym).transform_values(&:to_sym)
+ rescue JSON::ParserError
+ {}
+ end
+
+ def prompt(tool_name, args, terminal)
+ return true unless terminal.interactive?
+
+ answer = terminal.ask(" Allow? [Y/n] > ")&.downcase
+ raise "User denied permission: #{tool_name}" if answer == "n"
+
+ true
+ end
+
+ def default_rules
+ LOAD_PATHS.reduce({}) do |rules, path|
+ rules.merge(load_config(File.expand_path(path)))
+ end
+ end
+ end
+ end
+end
lib/elelem/tools.rb
@@ -6,6 +6,7 @@ require "tempfile"
require_relative "tools/version"
require_relative "tools/compact"
+require_relative "tools/confirm"
require_relative "tools/edit"
require_relative "tools/eval"
require_relative "tools/git"
@@ -14,6 +15,7 @@ require_relative "tools/grep"
require_relative "tools/init"
require_relative "tools/interview"
require_relative "tools/list"
+require_relative "tools/permissions"
require_relative "tools/shell"
require_relative "tools/task"
require_relative "tools/verify"
spec/elelem/tools/permissions_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Tools::Permissions do
+ subject { described_class.new }
+
+ let(:terminal) { double(ask: nil, interactive?: false) }
+
+ describe "#check" do
+ context "with default allow policies" do
+ it "allows read without prompting" do
+ expect(subject.check("read", {}, terminal: terminal)).to be true
+ expect(terminal).not_to have_received(:ask)
+ end
+ end
+
+ context "with deny policy" do
+ subject { described_class.new(rules: { write: :deny }) }
+
+ it { expect { subject.check("write", {}, terminal: terminal) }.to raise_error(/Permission denied/) }
+ end
+
+ context "with ask policy on a non-interactive terminal" do
+ it "returns true without prompting" do
+ expect(subject.check("execute", {}, terminal: terminal)).to be true
+ expect(terminal).not_to have_received(:ask)
+ end
+ end
+
+ context "with ask policy on an interactive terminal" do
+ let(:terminal) { double(ask: answer, interactive?: true) }
+
+ context "when approved" do
+ let(:answer) { "y" }
+
+ it "prompts and returns true" do
+ expect(subject.check("execute", {}, terminal: terminal)).to be true
+ expect(terminal).to have_received(:ask)
+ end
+ end
+
+ context "when denied" do
+ let(:answer) { "n" }
+
+ it { expect { subject.check("execute", {}, terminal: terminal) }.to raise_error(/User denied permission/) }
+ end
+ end
+ end
+end
spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../lib/elelem/tools"
+
+RSpec.configure do |config|
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
.rspec
@@ -0,0 +1,1 @@
+--require spec_helper
Gemfile
@@ -6,3 +6,4 @@ gemspec name: "elelem-tools"
gem "irb"
gem "rake", "~> 13.0"
+gem "rspec", "~> 3.0"
Gemfile.lock
@@ -12,6 +12,7 @@ GEM
base64 (0.3.0)
bigdecimal (4.1.2)
date (3.5.1)
+ diff-lcs (1.6.2)
digest (3.2.1)
elelem (0.10.0)
base64 (~> 0.1)
@@ -83,6 +84,19 @@ GEM
reline (0.7.0)
io-console (~> 0.5)
resolv (0.7.2)
+ rspec (3.13.2)
+ rspec-core (~> 3.13.0)
+ rspec-expectations (~> 3.13.0)
+ rspec-mocks (~> 3.13.0)
+ rspec-core (3.13.6)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.8)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-support (3.13.7)
securerandom (0.4.1)
shellwords (0.2.2)
simpleidn (0.3.0)
@@ -101,12 +115,14 @@ DEPENDENCIES
elelem-tools!
irb
rake (~> 13.0)
+ rspec (~> 3.0)
CHECKSUMS
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
digest (3.2.1) sha256=ab3312b4e272d7d5dc41c564c86a25861a1f34ac5153374199a0b74861395947
elelem (0.10.0) sha256=de67f3a28351640da471e6e80e7a1d79e83e293cbc502a9e98c3c609cad2e237
elelem-tools (0.1.1)
@@ -134,6 +150,11 @@ CHECKSUMS
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
resolv (0.7.2) sha256=626d044d975ab2daac759bf898416f1b51e2cb8dcd6727c2b5b5b28b97ead2e1
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
shellwords (0.2.2) sha256=b8695a791de2f71472de5abdc3f4332f6535a4177f55d8f99e7e44266cd32f94
simpleidn (0.3.0) sha256=12ca730bed2f3db04d11e9bfd1bca3e11fb37f55b21eb2e9793fb5814bf54d03
Rakefile
@@ -1,4 +1,8 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
-task default: %i[]
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: %i[spec]