Class: Rubino::Run::SessionApprovalCache
- Inherits:
-
Object
- Object
- Rubino::Run::SessionApprovalCache
- Defined in:
- lib/rubino/run/session_approval_cache.rb
Overview
Remembers approval decisions that should survive past the current call so the agent doesn’t re-prompt the user for the same operation in the same session.
Granularity: a decision is stored as a DERIVED RULE, not the exact “<tool>:<command>” string. The caller still passes a scope shaped like “shell:rm -rf /tmp/cache” or “write:report.md”; the cache splits it into (tool, command) and asks Security::PrefixDeriver for the rule to remember. This mirrors the reference, which keys session approvals on a PATTERN KEY rather than the raw command (approve_session / is_approved). The practical effect for S3:
- a DANGEROUS command remembers its pattern CLASS, so approving e.g.
`git push --force origin main` once also covers `git push -f other`
in the same session (same "git force push" class);
- a PLAIN command still remembers only the exact command, so approving
`git status` does NOT auto-approve `git diff` (narrow for S3; the
broad prefix rule is derived but wired into a decision only in S5).
A scope with no “:” (a tool-wide scope like “shell”) has no command to derive from and is stored/matched verbatim.
Persistence: in-memory, process-lifetime. “session” decisions die with the process; “always” would deserve disk persistence but isn’t wired up yet (S5), so we treat both as session-scoped for now.
Thread-safe: every read/write goes through @mutex.
Constant Summary collapse
- REMEMBERED_DECISIONS =
Decisions that should be persisted on approval.
%w[session always].freeze
Class Method Summary collapse
-
.instance ⇒ Object
Singleton accessor.
-
.reset_singleton! ⇒ Object
Resets the singleton — used by tests that need a clean slate.
Instance Method Summary collapse
-
#allowed?(session_id, scope) ⇒ Boolean
True when a prior decision for this session already covers the command carried by ‘scope` — pattern-class membership, prefix start_with?, or an exact-command match, per the stored rule kinds.
-
#forget!(session_id = nil) ⇒ Object
Drops every cached decision for one session (e.g. after a session is deleted).
-
#initialize ⇒ SessionApprovalCache
constructor
A new instance of SessionApprovalCache.
-
#remember(session_id, scope, decision) ⇒ Object
Records a decision for (session_id, scope) as a derived rule.
Constructor Details
#initialize ⇒ SessionApprovalCache
Returns a new instance of SessionApprovalCache.
49 50 51 52 |
# File 'lib/rubino/run/session_approval_cache.rb', line 49 def initialize @data = Hash.new { |h, k| h[k] = [] } # session_id => [Rule, ...] @mutex = Mutex.new end |
Class Method Details
.instance ⇒ Object
Singleton accessor. We don’t use Dry::Container or similar here because the cache is process-global state that the runner needs to inject into per-run UI::API instances; one shared object is the simplest expression of “remember across runs of the same session”.
36 37 38 |
# File 'lib/rubino/run/session_approval_cache.rb', line 36 def self.instance @instance ||= new end |
.reset_singleton! ⇒ Object
Resets the singleton — used by tests that need a clean slate. Avoids hidden cross-test leakage when specs share the process.
42 43 44 |
# File 'lib/rubino/run/session_approval_cache.rb', line 42 def self.reset_singleton! @instance = nil end |
Instance Method Details
#allowed?(session_id, scope) ⇒ Boolean
True when a prior decision for this session already covers the command carried by ‘scope` — pattern-class membership, prefix start_with?, or an exact-command match, per the stored rule kinds.
70 71 72 73 74 75 76 77 |
# File 'lib/rubino/run/session_approval_cache.rb', line 70 def allowed?(session_id, scope) return false unless session_id && scope command = scope_command(scope) @mutex.synchronize do @data[session_id.to_s].any? { |rule| rule.covers?(command) } end end |
#forget!(session_id = nil) ⇒ Object
Drops every cached decision for one session (e.g. after a session is deleted). Pass nil to wipe every session.
81 82 83 84 85 86 87 88 89 |
# File 'lib/rubino/run/session_approval_cache.rb', line 81 def forget!(session_id = nil) @mutex.synchronize do if session_id @data.delete(session_id.to_s) else @data.clear end end end |
#remember(session_id, scope, decision) ⇒ Object
Records a decision for (session_id, scope) as a derived rule. No-op when either value is blank, or the decision isn’t a remembered kind.
56 57 58 59 60 61 62 63 64 65 |
# File 'lib/rubino/run/session_approval_cache.rb', line 56 def remember(session_id, scope, decision) return unless session_id && scope return unless REMEMBERED_DECISIONS.include?(decision.to_s.downcase) rule = rule_for_scope(scope) @mutex.synchronize do rules = @data[session_id.to_s] rules << rule unless rules.any? { |r| r == rule } end end |