Class: Rigor::Plugin::IoBoundary
- Inherits:
-
Object
- Object
- Rigor::Plugin::IoBoundary
- Defined in:
- lib/rigor/plugin/io_boundary.rb,
sig/rigor/plugin/io_boundary.rbs
Overview
Analyzer-side helper plugins go through to read files and (eventually) reach the network. The boundary enforces the active TrustPolicy and accumulates a Cache::Descriptor of every read so plugin contributions stay invalidatable alongside their inputs.
ADR-2 ยง "Plugin Trust and I/O Policy" is the binding contract. The boundary is not a sandbox: a
plugin that uses File.read directly bypasses everything here, and the ADR explicitly accepts that
trade-off. The discipline is: when plugin code goes through this surface, reads stay within the trust
scope and feed the cache descriptor; contributions built on top of out-of-scope reads will not
invalidate correctly.
Slice 2 ships a minimal surface:
#read_file(path)โ validates against the policy, returns the file's contents, and adds a digest-keyed Cache::Descriptor::FileEntry to the boundary's accumulated descriptor.#open_url(url)โ fetches the URL when the policy permits it (network_policy: :allowlistplus anallowed_url_hostsmatch) and raises AccessDeniedError otherwise. v0.1.2 ships the allowlist surface; the default project policy still hasnetwork_policy: :disabledso plugins that want network access opt in explicitly through.rigor.yml'splugins_io.network: allowlistplusplugins_io.allowed_url_hosts: [...]. The HTTP fetch is GET-only over HTTPS, capped at URL_TIMEOUT_SECONDS wall time and URL_MAX_BYTES body size; non-2xx responses raise AccessDeniedError so plugin code doesn't have to rescue mid-build.#cache_descriptorโ flushes the accumulated entries into a fresh Cache::Descriptor for the contribution that built it. URL fetches contributeConfigEntryrows keyed"url:#{url}"with the response body's SHA-256 so contributions invalidate when the remote document changes.
Constant Summary collapse
- URL_TIMEOUT_SECONDS =
10- URL_MAX_BYTES =
10 * 1024 * 1024
Instance Attribute Summary collapse
-
#plugin_id ⇒ Object
readonly
Returns the value of attribute plugin_id.
-
#policy ⇒ Object
readonly
Returns the value of attribute policy.
Instance Method Summary collapse
-
#cache_descriptor ⇒ Object
Returns a frozen
Rigor::Cache::Descriptor(no RBS yet -> untyped). -
#initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new) ⇒ IoBoundary
constructor
A new instance of IoBoundary.
-
#open_url(url) ⇒ String
Fetches the URL when the policy permits it.
-
#read_file(path) ⇒ String
Reads the file at
pathafter validating it against the policy.
Constructor Details
#initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new) ⇒ IoBoundary
Returns a new instance of IoBoundary.
39 40 41 42 43 44 45 46 |
# File 'lib/rigor/plugin/io_boundary.rb', line 39 def initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new) @policy = policy @plugin_id = plugin_id.to_s.dup.freeze @file_entries = {} @config_entries = {} @http_client = http_client @mutex = Mutex.new end |
Instance Attribute Details
#plugin_id ⇒ Object (readonly)
Returns the value of attribute plugin_id.
37 38 39 |
# File 'lib/rigor/plugin/io_boundary.rb', line 37 def plugin_id @plugin_id end |
#policy ⇒ Object (readonly)
Returns the value of attribute policy.
37 38 39 |
# File 'lib/rigor/plugin/io_boundary.rb', line 37 def policy @policy end |
Instance Method Details
#cache_descriptor ⇒ Object
Returns a frozen Rigor::Cache::Descriptor (no RBS yet -> untyped).
92 93 94 95 |
# File 'lib/rigor/plugin/io_boundary.rb', line 92 def cache_descriptor files, configs = @mutex.synchronize { [@file_entries.values.dup, @config_entries.values.dup] } Cache::Descriptor.new(files: files, configs: configs) end |
#open_url(url) ⇒ String
Fetches the URL when the policy permits it. Returns the response body. Raises AccessDeniedError when
the policy is :disabled, the URL scheme is not https, the host is not on the allowlist, the
response is non-2xx, the body exceeds URL_MAX_BYTES, or the request times out
(URL_TIMEOUT_SECONDS). On success, records a ConfigEntry keyed "url:#{url}" with the body's
SHA-256 so the cache descriptor invalidates if the remote document changes.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/rigor/plugin/io_boundary.rb', line 72 def open_url(url) url_string = url.to_s unless @policy.allow_url?(url_string) raise AccessDeniedError.new( "plugin #{@plugin_id.inspect} cannot open URL #{url.inspect}: " \ "URL is not permitted by the active TrustPolicy " \ "(network_policy=#{@policy.network_policy} allowed_url_hosts=#{@policy.allowed_url_hosts.inspect})", reason: :network_disabled, resource: url_string ) end body = @http_client.get(url_string, timeout: URL_TIMEOUT_SECONDS, max_bytes: URL_MAX_BYTES) record_url_entry(url_string, body) body end |
#read_file(path) ⇒ String
Reads the file at path after validating it against the policy. Raises AccessDeniedError when the
path is outside every allowed read root. Records a :digest FileEntry so the resulting cache slice
invalidates on content change.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/rigor/plugin/io_boundary.rb', line 51 def read_file(path) absolute = File.(path.to_s) unless @policy.allow_read?(absolute) raise AccessDeniedError.new( "plugin #{@plugin_id.inspect} cannot read #{absolute.inspect}: " \ "path is outside the trusted-read scope", reason: :read_outside_scope, resource: absolute ) end contents = File.binread(absolute) record_file_entry(absolute, contents) contents end |