ask-agent 0.30.1 → 0.31.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 +21 -0
- data/lib/ask/agent/policies/approval_policy.rb +24 -6
- data/lib/ask/agent/policies/permission_rules.rb +130 -0
- data/lib/ask/agent/session.rb +1 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e039a90d18224ab29b454dd2094a81628ddf80cc39938419038378182ecbe472
|
|
4
|
+
data.tar.gz: 8d6d1a8005d2451469a998d36061adea945245beef774876b5b5bc1565f8fd5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 014b65f0247e65970d3d4fa7496744aa7d38f00f2684c53ff02f74ec5c547b6534c906b24dc6594387069e5034b1916e7f4e3a2986c6bf55e42412177ac7a678
|
|
7
|
+
data.tar.gz: 72f571ba1ffafca8f9e9f181d51891ce9ea315e4ca5ebc15e1bf27557e8c05faf9ed6f3136a0a88a124f6833f096b5bb7d6cc9d7db9b32bc1dc0a27eb6cff1a9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [0.31.0] — 2026-08-06
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Permission rules — persisted allow/ask/deny patterns for tool calls.**
|
|
6
|
+
`Ask::Agent::Policies::PermissionRules` classifies every call before it
|
|
7
|
+
executes or prompts, so "approve once, remember the pattern" replaces
|
|
8
|
+
per-call prompting:
|
|
9
|
+
- DSL in declaration order (first match wins): `allow`, `ask`, `deny`
|
|
10
|
+
with a tool pattern (String, Symbol, Regexp, or `:all`) and an optional
|
|
11
|
+
argument pattern (Regexp, substring, or `nil` for any).
|
|
12
|
+
- Wire in via `Session.new(approval: { rules: rules })`. Rules take
|
|
13
|
+
precedence over a tool's own `approval_required` / `auto_approvable`
|
|
14
|
+
declarations: `:deny` blocks, `:allow` proceeds without the queue,
|
|
15
|
+
`:ask` queues regardless of auto-approvable.
|
|
16
|
+
- **Dangerous-rule guard**: an unrestricted `:allow` on a code-executing
|
|
17
|
+
tool (`bash`, `code`, `repl`, or `:all`) is downgraded to `:ask` unless
|
|
18
|
+
the ruleset is created with `auto_allow_dangerous: true` — "approve
|
|
19
|
+
once" can't become "approve anything". `dangerous_rules` reports which
|
|
20
|
+
rules were affected.
|
|
21
|
+
|
|
1
22
|
## [0.30.1] — 2026-08-06
|
|
2
23
|
|
|
3
24
|
### Fixed
|
|
@@ -32,13 +32,18 @@ module Ask
|
|
|
32
32
|
# rule-based classification on top of the tool's own declaration.
|
|
33
33
|
# Strings match tool names exactly, Regexps match against the name,
|
|
34
34
|
# and :all requires approval for every tool call.
|
|
35
|
+
# @param rules [Ask::Agent::Policies::PermissionRules, nil] persisted
|
|
36
|
+
# allow/ask/deny patterns. Rules classify first and take precedence
|
|
37
|
+
# over tool declarations: :deny blocks, :allow proceeds without the
|
|
38
|
+
# queue, :ask queues regardless of auto-approvable.
|
|
35
39
|
# @param tools [Array<Object>, nil] the session's resolved tool
|
|
36
40
|
# instances, used to read class-level declarations
|
|
37
41
|
# (`approval_required`, `auto_approvable`). When nil, only the
|
|
38
42
|
# rule-based lists classify calls.
|
|
39
|
-
def initialize(queue:, require_approval: nil, tools: nil)
|
|
43
|
+
def initialize(queue:, require_approval: nil, rules: nil, tools: nil)
|
|
40
44
|
@queue = queue
|
|
41
45
|
@require_approval = require_approval
|
|
46
|
+
@rules = rules
|
|
42
47
|
@tools = Array(tools)
|
|
43
48
|
end
|
|
44
49
|
|
|
@@ -46,12 +51,27 @@ module Ask
|
|
|
46
51
|
#
|
|
47
52
|
# @param tool_call [Ask::Agent::ToolCallInfo]
|
|
48
53
|
# @param _context [Hash]
|
|
49
|
-
# @return [Hash] {action: :proceed} to run,
|
|
50
|
-
#
|
|
54
|
+
# @return [Hash] {action: :proceed} to run, {action: :pending,
|
|
55
|
+
# action_id:, reason:} to queue for approval, or {action: :block,
|
|
56
|
+
# reason:} to refuse
|
|
51
57
|
def before_tool_call(tool_call, _context)
|
|
58
|
+
case @rules&.classify(tool_call.name, tool_call.arguments)
|
|
59
|
+
when :deny
|
|
60
|
+
return { action: :block, reason: "Denied by permission rules: '#{tool_call.name}'" }
|
|
61
|
+
when :allow
|
|
62
|
+
return { action: :proceed }
|
|
63
|
+
when :ask
|
|
64
|
+
return queue_for_approval(tool_call, auto_approvable: false)
|
|
65
|
+
end
|
|
66
|
+
|
|
52
67
|
return { action: :proceed } unless approval_required?(tool_call.name)
|
|
53
68
|
|
|
54
|
-
auto_approvable
|
|
69
|
+
queue_for_approval(tool_call, auto_approvable: tool_auto_approvable?(tool_call.name))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def queue_for_approval(tool_call, auto_approvable:)
|
|
55
75
|
action_id = @queue.submit(
|
|
56
76
|
tool_call_id: tool_call.id,
|
|
57
77
|
tool_name: tool_call.name,
|
|
@@ -63,8 +83,6 @@ module Ask
|
|
|
63
83
|
{ action: :pending, action_id: action_id, reason: "Tool '#{tool_call.name}' requires approval" }
|
|
64
84
|
end
|
|
65
85
|
|
|
66
|
-
private
|
|
67
|
-
|
|
68
86
|
def approval_required?(tool_name)
|
|
69
87
|
return true if @require_approval == :all
|
|
70
88
|
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
data/lib/ask/agent/session.rb
CHANGED
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Ask
|
|
|
35
35
|
autoload :RateLimiter, "ask/agent/policies/rate_limiter"
|
|
36
36
|
autoload :AuditLog, "ask/agent/policies/audit_log"
|
|
37
37
|
autoload :ApprovalPolicy, "ask/agent/policies/approval_policy"
|
|
38
|
+
autoload :PermissionRules, "ask/agent/policies/permission_rules"
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
autoload :ToolCallRepair, "ask/agent/tool_call_repair"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -189,6 +189,7 @@ files:
|
|
|
189
189
|
- lib/ask/agent/policies/approval_policy.rb
|
|
190
190
|
- lib/ask/agent/policies/audit_log.rb
|
|
191
191
|
- lib/ask/agent/policies/audit_log/active_record_writer.rb
|
|
192
|
+
- lib/ask/agent/policies/permission_rules.rb
|
|
192
193
|
- lib/ask/agent/policies/permissions.rb
|
|
193
194
|
- lib/ask/agent/policies/rate_limiter.rb
|
|
194
195
|
- lib/ask/agent/reflector.rb
|