Class: Nexo::Permissions
- Inherits:
-
Object
- Object
- Nexo::Permissions
- Defined in:
- lib/nexo/permissions.rb
Overview
The authorization gate for a sandbox's capabilities. Each tool asks
authorize! before it touches the sandbox, so gating is provider-independent
and does not rely on any framework callback.
Modes:
:auto— allow everything.:read_only— allow +:read+/+:glob+, deny +:write+/+:shell+/+:fetch+/+:search+ (the default).:ask— defer toon_ask; a truthy return allows, anything else denies.:approve— durable, cross-process sibling of:ask(Spec 16): with nodecisionit raises Nexo::ApprovalRequired (→ Workflow#run_agent suspends the run); with{approved: true}it allows, with{approved: false}it Denied denies.
Capabilities are :read, :glob, :write, :shell, :fetch, :search.
Anything listed in allow: is permitted regardless of mode.
Defined Under Namespace
Classes: Denied
Constant Summary collapse
- MODES =
The recognized permission modes:
:auto,:read_only,:ask,:approve. %i[auto read_only ask approve].freeze
Instance Attribute Summary collapse
-
#decision ⇒ Object
The approval decision under
:approve(Spec 16):nil(undecided ⇒ suspend) or a{approved: true|false}Hash. -
#mode ⇒ Object
readonly
The configured Nexo permission mode (one of MODES).
Instance Method Summary collapse
-
#authorize!(capability, detail = nil) ⇒ Object
Authorizes
capability(with optionaldetailpassed to an:askhook). -
#authorize_mcp!(tool_name, args = {}) ⇒ Object
Authorizes an MCP tool call by name.
-
#initialize(mode: :read_only, allow: %i[read glob],, mcp_allow: [], on_ask: nil, ask_when: nil, approve_when: nil, decision: nil) ⇒ Permissions
constructor
ask_when:is an optional->(capability, detail)predicate that scopes which actions actually prompt under:ask(and, unchanged, under:approve): when it returns falsey the action is auto-allowed without callingon_ask/ requiring a decision; truthy (or when unset) falls through toon_ask/ the approval gate exactly as before. -
#with_decision(decision) ⇒ Object
Returns a copy of this gate carrying
decision(a{approved: …}Hash ornil), leaving the receiver untouched (Spec 16).
Constructor Details
#initialize(mode: :read_only, allow: %i[read glob],, mcp_allow: [], on_ask: nil, ask_when: nil, approve_when: nil, decision: nil) ⇒ Permissions
ask_when: is an optional ->(capability, detail) predicate that scopes
which actions actually prompt under :ask (and, unchanged, under
:approve): when it returns falsey the action is auto-allowed without
calling on_ask / requiring a decision; truthy (or when unset) falls
through to on_ask / the approval gate exactly as before. It only ever
narrows what is auto-allowed from the "ask/approve for everything"
baseline — it never widens authority. Applies to #authorize! only, not
#authorize_mcp!. approve_when: is an alias that maps onto the same
predicate (there is one predicate, not two — Spec 16 Q4).
decision: (default nil) seeds the :approve decision (see #decision).
50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/nexo/permissions.rb', line 50 def initialize(mode: :read_only, allow: %i[read glob], mcp_allow: [], on_ask: nil, ask_when: nil, approve_when: nil, decision: nil) raise ArgumentError, "unknown mode #{mode}" unless MODES.include?(mode) @mode = mode @allow = allow @mcp_allow = mcp_allow.map(&:to_s) @on_ask = on_ask @ask_when = ask_when || approve_when @decision = decision end |
Instance Attribute Details
#decision ⇒ Object
The approval decision under :approve (Spec 16): nil (undecided ⇒
suspend) or a {approved: true|false} Hash. Writable after construction so
Workflow#run_agent can thread a resume decision into an
already-resolved :approve gate without rebuilding it. Ignored by every
other mode.
37 38 39 |
# File 'lib/nexo/permissions.rb', line 37 def decision @decision end |
#mode ⇒ Object (readonly)
The configured Nexo permission mode (one of MODES). Read by the agent to
map onto an opt-in backend's own permission vocabulary (see
Agent#permission_mode).
30 31 32 |
# File 'lib/nexo/permissions.rb', line 30 def mode @mode end |
Instance Method Details
#authorize!(capability, detail = nil) ⇒ Object
Authorizes capability (with optional detail passed to an :ask hook).
Returns true when allowed; raises Denied otherwise.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/nexo/permissions.rb', line 72 def (capability, detail = nil) return true if @allow.include?(capability) case @mode when :auto true when :read_only if %i[write shell fetch search].include?(capability) raise Denied, "#{capability} denied in read_only mode" end true when :ask # Scoped-ask: when ask_when says this action doesn't need a prompt, # auto-allow without calling on_ask. Unset ask_when = ask for everything. return true if @ask_when && !@ask_when.call(capability, detail) unless @on_ask&.call(capability, detail) raise Denied, "#{capability} (#{detail}) denied by user" end true when :approve # Durable approval (Spec 16). Scoped-approve: when ask_when says this # action doesn't need approval, auto-allow without a decision. Unset # ask_when = approve for everything. return true if @ask_when && !@ask_when.call(capability, detail) if @decision.nil? # Undecided ⇒ pause the run for a human (Branch A): the signal # propagates out of the tool loop; run_agent turns it into a suspend. raise Nexo::ApprovalRequired.new(capability, detail) elsif @decision[:approved] true else # Explicitly denied on resume ⇒ never auto-allow; the tool rescues # Denied into {error:} and the model adapts. raise Denied, "#{capability} (#{detail}) not approved" end end end |
#authorize_mcp!(tool_name, args = {}) ⇒ Object
Authorizes an MCP tool call by name. A deliberate sibling of #authorize!
on a separate capability axis: an MCP tool runs inside the MCP server,
outside the sandbox, so this gates the authority to invoke it — a different
guarantee than sandbox capability. Fails closed under :read_only (nothing
allowed unless the exact tool_name is listed in mcp_allow).
:auto— allow every MCP tool.:read_only— allow only names inmcp_allow(default[]⇒ deny all).:ask— defer toon_askwith(:mcp, {tool:, args:}); a truthy return allows, anything else denies.:approve— durable sibling of:askon the MCP axis: names inmcp_alloware pre-approved, anything else needs a decision — undecided raises Nexo::ApprovalRequired (→ Workflow#run_agent suspends),approvedallows,approved: falseDenies.
Returns true when allowed; raises Denied otherwise. The else is a
fail-closed backstop: a future mode that forgets to extend this gate denies
by default rather than silently allowing (the bug this replaced).
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/nexo/permissions.rb', line 130 def (tool_name, args = {}) name = tool_name.to_s case @mode when :auto true when :read_only return true if @mcp_allow.include?(name) raise Denied, "mcp tool #{name} denied in read_only mode (not in mcp_allow)" when :ask unless @on_ask&.call(:mcp, {tool: name, args: args}) raise Denied, "mcp tool #{name} denied by user" end true when :approve # Pre-approved read tools pass; everything else routes through the same # decision gate as #authorize!'s :approve branch. return true if @mcp_allow.include?(name) if @decision.nil? raise Nexo::ApprovalRequired.new(:mcp, name, args) elsif @decision[:approved] true else raise Denied, "mcp tool #{name} not approved" end else raise Denied, "mcp tool #{name} denied (unhandled mode #{@mode})" end end |
#with_decision(decision) ⇒ Object
Returns a copy of this gate carrying decision (a {approved: …} Hash or
nil), leaving the receiver untouched (Spec 16). Used to thread a per-run
resume decision into a user-supplied, class-level :approve Permissions
without mutating the shared instance.
66 67 68 |
# File 'lib/nexo/permissions.rb', line 66 def with_decision(decision) dup.tap { |copy| copy.decision = decision } end |