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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +29 -0
- data/lib/ask/agent/exit_plan_mode.rb +1 -1
- data/lib/ask/agent/session.rb +247 -28
- data/lib/ask/agent/session_adapter.rb +119 -1
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +4 -7
- metadata +15 -5
- data/lib/ask/agent/approval_queue.rb +0 -249
- data/lib/ask/agent/policies/approval_policy.rb +0 -114
- data/lib/ask/agent/policies/permission_rules.rb +0 -130
- data/lib/ask/agent/policies/permissions.rb +0 -86
|
@@ -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
|