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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf67fcc30605c8d4752a97133a13a7fbe500c32a68c47640552bc30cf6dbea5b
4
- data.tar.gz: 638931a8c1c2c5ae0ca5535e9e78e9252910106af9134366a455523925b6d895
3
+ metadata.gz: e039a90d18224ab29b454dd2094a81628ddf80cc39938419038378182ecbe472
4
+ data.tar.gz: 8d6d1a8005d2451469a998d36061adea945245beef774876b5b5bc1565f8fd5a
5
5
  SHA512:
6
- metadata.gz: 01ca01b502aa5789eef910f50626085bb6675e86dedd435b189b0e928d592ac27c48c145ff5c159580c2a9854528160c86074031dccd971b033b75b6f42bf3fa
7
- data.tar.gz: 6bf3ebc3428cf64c8a1ee7e958e45c349669e4eaba92a8bc5d90b522f0b9a47d4f3743570db235b8d9b4e526d3ea470eb1a81795acc6492fd67afda4b59d9b70
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, or
50
- # {action: :pending, action_id:, reason:} to queue for approval
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 = tool_auto_approvable?(tool_call.name)
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
@@ -555,6 +555,7 @@ module Ask
555
555
  policy = Ask::Agent::Policies::ApprovalPolicy.new(
556
556
  queue: queue,
557
557
  require_approval: policy_opts[:require_approval],
558
+ rules: policy_opts[:rules],
558
559
  tools: @tools
559
560
  )
560
561
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.30.1"
5
+ VERSION = "0.31.0"
6
6
  end
7
7
  end
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.30.1
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