ask-agent 0.30.0 → 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 +37 -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 +18 -6
- 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,40 @@
|
|
|
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
|
+
|
|
22
|
+
## [0.30.1] — 2026-08-06
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Framework-injected tools are no longer persisted in session metadata.**
|
|
27
|
+
The built-in `load_skill` tool was saved alongside user tools and could
|
|
28
|
+
not be auto-instantiated on load (`LoadSkillTool` requires a registry), so
|
|
29
|
+
the previous 0.30.0 fix skipped it with a broad rescue. Persisted metadata
|
|
30
|
+
now contains only user-supplied tools (`persisted_tools`); `load_skill` is
|
|
31
|
+
re-added by `resolve_tools` per session with a proper registry.
|
|
32
|
+
- **`Session.load` no longer swallows tool-restore failures silently.** The
|
|
33
|
+
safety net now rescues only `NameError` (renamed/removed classes) and
|
|
34
|
+
`ArgumentError` (constructors with required args), and warns with the
|
|
35
|
+
tool name instead of failing the whole load — genuine tool bugs surface
|
|
36
|
+
instead of disappearing.
|
|
37
|
+
|
|
1
38
|
## [0.30.0] — 2026-08-06
|
|
2
39
|
|
|
3
40
|
### Added
|
|
@@ -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
|
@@ -314,14 +314,16 @@ module Ask
|
|
|
314
314
|
session = new(
|
|
315
315
|
id: data[:id],
|
|
316
316
|
model: data.dig(:metadata, :model),
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
317
|
+
# Restore saved user tools by class name. Tools that cannot be
|
|
318
|
+
# restored — renamed/removed classes (NameError) or constructors
|
|
319
|
+
# with required args (ArgumentError) — are skipped with a warning
|
|
320
|
+
# instead of failing the whole load; resolve_tools re-adds the
|
|
321
|
+
# framework-injected load_skill tool with a proper registry.
|
|
321
322
|
tools: data.dig(:metadata, :tools).to_a.filter_map do |name|
|
|
322
323
|
begin
|
|
323
324
|
name.constantize.new
|
|
324
|
-
rescue
|
|
325
|
+
rescue NameError, ArgumentError => e
|
|
326
|
+
warn "[ask-agent] Session.load skipped tool '#{name}': #{e.class}: #{e.message}"
|
|
325
327
|
nil
|
|
326
328
|
end
|
|
327
329
|
end,
|
|
@@ -553,6 +555,7 @@ module Ask
|
|
|
553
555
|
policy = Ask::Agent::Policies::ApprovalPolicy.new(
|
|
554
556
|
queue: queue,
|
|
555
557
|
require_approval: policy_opts[:require_approval],
|
|
558
|
+
rules: policy_opts[:rules],
|
|
556
559
|
tools: @tools
|
|
557
560
|
)
|
|
558
561
|
|
|
@@ -694,6 +697,15 @@ module Ask
|
|
|
694
697
|
target.instance_variable_set(:@turn_count, data.dig(:metadata, :turn_count) || 0)
|
|
695
698
|
end
|
|
696
699
|
|
|
700
|
+
# User-supplied tools only. Framework-injected tools (the built-in
|
|
701
|
+
# load_skill tool) are re-created by resolve_tools on every session, so
|
|
702
|
+
# persisting them would leak framework internals into user data — and
|
|
703
|
+
# they cannot be auto-instantiated on load anyway (LoadSkillTool needs
|
|
704
|
+
# a registry).
|
|
705
|
+
def persisted_tools
|
|
706
|
+
@tools.reject { |t| t.is_a?(Ask::Skills::LoadSkillTool) }
|
|
707
|
+
end
|
|
708
|
+
|
|
697
709
|
def persist!
|
|
698
710
|
payload = {
|
|
699
711
|
id: @id,
|
|
@@ -707,7 +719,7 @@ module Ask
|
|
|
707
719
|
},
|
|
708
720
|
metadata: {
|
|
709
721
|
model: @chat.model.respond_to?(:id) ? @chat.model.id : @chat.model,
|
|
710
|
-
tools:
|
|
722
|
+
tools: persisted_tools.map { |t| t.class.name },
|
|
711
723
|
max_turns: @max_turns,
|
|
712
724
|
turn_count: @turn_count,
|
|
713
725
|
created_at: @created_at.iso8601,
|
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
|