ask-agent 0.40.29 → 0.41.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.
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Ask
6
- module Agent
7
- module Policies
8
- # Persisted, matchable allow/ask/deny patterns for tool calls.
9
- #
10
- # Rules classify a tool call before it executes or prompts:
11
- #
12
- # rules = Ask::Agent::Policies::PermissionRules.new do |r|
13
- # r.allow :bash, /^git (pull|push|status)/
14
- # r.ask :bash, /^rm -rf/
15
- # r.deny :write, %r{/\.env(\.local)?$}
16
- # r.ask :destroy, :all
17
- # end
18
- #
19
- # session = Ask::Agent::Session.new(
20
- # model: "gpt-4o",
21
- # approval: { rules: rules }
22
- # )
23
- #
24
- # Classification is first-match-wins, in declaration order: :allow runs
25
- # the tool, :ask queues it for human approval, :deny blocks it. Rules
26
- # take precedence over a tool's own `approval_required` / `auto_approvable`
27
- # declarations — they are explicit user intent.
28
- #
29
- # Dangerous-rule guard: an :allow rule for a code-executing tool (bash,
30
- # code, repl) whose argument pattern is unrestricted would let the model
31
- # run anything without asking. Such rules are downgraded to :ask unless
32
- # the ruleset was created with `auto_allow_dangerous: true` — "approve
33
- # once, remember the pattern" must not become "approve everything".
34
- class PermissionRules
35
- # A single rule: tool pattern + optional argument pattern + decision.
36
- Rule = Data.define(:tool_pattern, :argument_pattern, :decision) do
37
- def matches?(tool_name, arguments)
38
- tool_matches?(tool_name) && argument_matches?(arguments)
39
- end
40
-
41
- def tool_matches?(tool_name)
42
- case tool_pattern
43
- when :all then true
44
- when Regexp then tool_pattern.match?(tool_name.to_s)
45
- else tool_pattern.to_s == tool_name.to_s
46
- end
47
- end
48
-
49
- def argument_matches?(arguments)
50
- return true if argument_pattern.nil?
51
-
52
- text = arguments.is_a?(Hash) ? JSON.generate(arguments) : arguments.to_s
53
- case argument_pattern
54
- when Regexp then argument_pattern.match?(text)
55
- else text.include?(argument_pattern.to_s)
56
- end
57
- end
58
-
59
- def universal?
60
- argument_pattern.nil?
61
- end
62
- end
63
-
64
- # Tools that execute arbitrary code — an unrestricted :allow rule on
65
- # any of these is dangerous.
66
- DANGEROUS_TOOLS = %i[bash code repl].freeze
67
-
68
- # @param auto_allow_dangerous [Boolean] keep unrestricted :allow
69
- # rules on code-executing tools (default false — they downgrade to
70
- # :ask)
71
- def initialize(auto_allow_dangerous: false, &block)
72
- @auto_allow_dangerous = auto_allow_dangerous
73
- @rules = []
74
- instance_eval(&block) if block
75
- end
76
-
77
- # DSL — declare rules in priority order (first match wins).
78
- def allow(tool_pattern, argument_pattern = nil)
79
- add(:allow, tool_pattern, argument_pattern)
80
- end
81
-
82
- def ask(tool_pattern, argument_pattern = nil)
83
- add(:ask, tool_pattern, argument_pattern)
84
- end
85
-
86
- def deny(tool_pattern, argument_pattern = nil)
87
- add(:deny, tool_pattern, argument_pattern)
88
- end
89
-
90
- # @return [Array<Rule>] declared rules, in order
91
- def rules = @rules.dup
92
-
93
- # Classify a tool call.
94
- #
95
- # @param tool_name [String]
96
- # @param arguments [Hash, String, nil] tool arguments (hash or JSON)
97
- # @return [Symbol, nil] :allow, :ask, :deny — or nil when no rule
98
- # matches
99
- def classify(tool_name, arguments = nil)
100
- rule = @rules.find { |r| r.matches?(tool_name, arguments) }
101
- return nil unless rule
102
-
103
- if rule.decision == :allow && dangerous?(rule) && !@auto_allow_dangerous
104
- :ask
105
- else
106
- rule.decision
107
- end
108
- end
109
-
110
- # @return [Array<Rule>] rules that would allow unrestricted execution
111
- # of a code-executing tool
112
- def dangerous_rules
113
- @rules.select { |r| dangerous?(r) }
114
- end
115
-
116
- private
117
-
118
- def add(decision, tool_pattern, argument_pattern)
119
- @rules << Rule.new(tool_pattern, argument_pattern, decision)
120
- end
121
-
122
- def dangerous?(rule)
123
- return false unless rule.decision == :allow && rule.universal?
124
-
125
- rule.tool_pattern == :all || DANGEROUS_TOOLS.any? { |t| rule.tool_matches?(t) }
126
- end
127
- end
128
- end
129
- end
130
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- module Agent
5
- module Policies
6
- class Permissions
7
- DEFAULT_TOOLS = %i[write edit bash destroy].freeze
8
-
9
- ACCESS_MODES = {
10
- full_access: { blocked_tools: [] }.freeze,
11
- ask_before_changes: { blocked_tools: DEFAULT_TOOLS }.freeze,
12
- read_only: { blocked_tools: %i[write edit bash destroy] }.freeze
13
- }.freeze
14
-
15
- def initialize(mode: nil, blocked_tools: nil, timeout: nil)
16
- @mode = mode
17
- @timeout = timeout
18
- @pending = {}
19
- @mutex = Mutex.new
20
-
21
- @blocked_tools = if mode
22
- config = ACCESS_MODES[mode]
23
- raise ArgumentError, "Unknown access mode: #{mode.inspect}. Valid: #{ACCESS_MODES.keys.join(', ')}" unless config
24
- config[:blocked_tools].dup
25
- elsif blocked_tools
26
- Array(blocked_tools).map(&:to_sym)
27
- else
28
- DEFAULT_TOOLS.dup
29
- end
30
- end
31
-
32
- def before_tool_call(tool_call, _context)
33
- return { action: :proceed } unless @blocked_tools.include?(tool_call.name.to_sym)
34
-
35
- if approved?(tool_call)
36
- { action: :proceed }
37
- else
38
- request_approval(tool_call)
39
- end
40
- end
41
-
42
- def approve(tool_call_id)
43
- @mutex.synchronize do
44
- entry = @pending[tool_call_id]
45
- return false unless entry
46
- entry[:approved] = true
47
- end
48
- end
49
-
50
- def pending_approvals
51
- @mutex.synchronize { @pending.values.reject { |e| e[:approved] } }
52
- end
53
-
54
- private
55
-
56
- def approved?(tool_call)
57
- @mutex.synchronize do
58
- key = tool_call.id
59
- entry = @pending[key]
60
- return false unless entry
61
-
62
- if @timeout && (Time.now - entry[:created_at]) > @timeout
63
- @pending.delete(key)
64
- return false
65
- end
66
-
67
- entry[:approved]
68
- end
69
- end
70
-
71
- def request_approval(tool_call)
72
- @mutex.synchronize do
73
- @pending[tool_call.id] = {
74
- tool_call: tool_call,
75
- approved: false,
76
- created_at: Time.now
77
- }
78
- end
79
-
80
- warn "[Permissions] Tool '#{tool_call.name}' requires approval. Call approve('#{tool_call.id}') to allow."
81
- { action: :block, reason: "Tool '#{tool_call.name}' requires approval" }
82
- end
83
- end
84
- end
85
- end
86
- end