visiq 0.1.2-x86_64-linux → 0.1.4-x86_64-linux
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/README.md +42 -2
- data/lib/visiq/governor.rb +261 -0
- data/lib/visiq.rb +37 -6
- data/vendor/libvisiq_core.so +0 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4dbb0d7342d5cb72587db4bf9e6424fbd6272ab11b522a7648b94afdbc60491
|
|
4
|
+
data.tar.gz: f3ccae163b896a6f472bea95e81922e593f67a6930557cafba6364c07136ba2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bba89fb5fe5b4944ebdea892b5ac6ae9f2a9af6fb6978a3280a0f7bed14f5e1ac684298df780625e749395f4da459ad2c443723027a299c59e61a4f2ff5b14c5
|
|
7
|
+
data.tar.gz: d8df3f3ef9389acde4c5ed5c798a57a0cf9c0aa7da7a19de49d45e9592e0594ac4ebebc247db57bb570a9dc439bc763332a33868601b23e6b63724dfb698f6bb
|
data/README.md
CHANGED
|
@@ -7,7 +7,8 @@ in-process by the compiled VisIQ core that every VisIQ SDK shares (the same engi
|
|
|
7
7
|
behind `@visiq/harness`), bound to Ruby through the stdlib `Fiddle` FFI. The engine
|
|
8
8
|
ships as **compiled machine code** (closed); the Ruby is thin FFI plumbing only.
|
|
9
9
|
|
|
10
|
-
- **Local & fast
|
|
10
|
+
- **Local & fast**: every decision is in-process. `Visiq::Governor` fetches the
|
|
11
|
+
agent's rules from the control plane and keeps them current.
|
|
11
12
|
- **Fail-open by default (G001 rescope, owner 2026-07-15)** — a harness-internal
|
|
12
13
|
failure (the core can't load, an FFI error) returns a loud fail-open permit;
|
|
13
14
|
set `VISIQ_FAIL_MODE=closed` to block instead. Malformed input and explicit
|
|
@@ -25,7 +26,46 @@ Native, per-platform "fat" gems are published for macOS (arm64, x86_64) and Linu
|
|
|
25
26
|
(x86_64, aarch64) — `gem install` selects the one matching your platform and it
|
|
26
27
|
bundles the compiled core. **Ruby ≥ 3.0.**
|
|
27
28
|
|
|
28
|
-
## Usage
|
|
29
|
+
## Usage (recommended): the self-refreshing `Visiq::Governor`
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "visiq"
|
|
33
|
+
|
|
34
|
+
# One per agent, kept for the life of the process. Reads VISIQ_API_KEY (and
|
|
35
|
+
# VISIQ_ENDPOINT for on-premises) from the environment when not passed here.
|
|
36
|
+
governor = Visiq::Governor.new(agent_id: "agent-1")
|
|
37
|
+
|
|
38
|
+
d = governor.gate_action(tool_name: "wire_transfer", args: { "amount" => 999 })
|
|
39
|
+
block_the_call unless d["allowed"]
|
|
40
|
+
r = governor.gate_retrieval(resource_metadata: { "classification" => "ssn" })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `Governor` fetches `GET /rules/bundle?agent_id=...` itself, fetches it again
|
|
44
|
+
before a decision once it is more than 5 seconds old (sending `If-None-Match`, so
|
|
45
|
+
an unchanged bundle costs a 304), and decides in-process against it. So a rule
|
|
46
|
+
change, or the agent being put in Agent Jail, reaches the next call. A failed or
|
|
47
|
+
timed-out fetch (2.5 s) keeps the rules already held, so a jail stays in force
|
|
48
|
+
through a control-plane outage. With no rules yet (first fetch failed, or no API
|
|
49
|
+
key configured) calls are permitted with a loud `[VisIQ] FAIL-OPEN (no-bundle)`
|
|
50
|
+
stderr line; `VISIQ_FAIL_MODE=closed` (or `fail_mode: "closed"`) denies them
|
|
51
|
+
instead. Refresh is lazy, on the calling thread, one fetch at a time; there is
|
|
52
|
+
no background thread.
|
|
53
|
+
|
|
54
|
+
| Option | Environment variable | Default |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `endpoint:` | `VISIQ_ENDPOINT`, then `VISIQ_BASE_URL` | `https://api.visiqlabs.com` when an API key is set |
|
|
57
|
+
| `api_key:` | `VISIQ_API_KEY`, then `VISIQ_ALLOW_API_KEY` | none |
|
|
58
|
+
| `agent_id:` | `VISIQ_AGENT_ID` | `agent` |
|
|
59
|
+
| `fail_mode:` | `VISIQ_FAIL_MODE` | `open` |
|
|
60
|
+
|
|
61
|
+
`governor.decide(event)` decides any event; `governor.bundle` returns the
|
|
62
|
+
(frozen) bundle in use.
|
|
63
|
+
|
|
64
|
+
## Usage: if you fetch the bundle yourself
|
|
65
|
+
|
|
66
|
+
The module functions decide against a bundle you pass in and make no network
|
|
67
|
+
calls. Fetch the bundle again at least every few seconds: a kill switch such as
|
|
68
|
+
Agent Jail reaches the agent only through a fresh bundle.
|
|
29
69
|
|
|
30
70
|
```ruby
|
|
31
71
|
require "visiq"
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Visiq
|
|
8
|
+
# The self-refreshing VisIQ client for Ruby: it fetches the agent's rule bundle
|
|
9
|
+
# from the control plane itself, keeps it fresh, and decides every call through
|
|
10
|
+
# the same compiled core as Visiq.gate_action.
|
|
11
|
+
#
|
|
12
|
+
# Why it exists. The module functions (Visiq.gate_action and friends) decide
|
|
13
|
+
# against whatever bundle the caller passes in. When an operator puts an agent
|
|
14
|
+
# in Agent Jail, GET /rules/bundle starts serving a terminal bundle
|
|
15
|
+
# ("shutdown": true) and the core denies every facet for it, but a program that
|
|
16
|
+
# fetched once at startup never sees that bundle, so the jailed agent keeps
|
|
17
|
+
# acting. A Governor re-fetches on its own, so the jail reaches the next call.
|
|
18
|
+
#
|
|
19
|
+
# The contract (mirrors the Python Governor and the TypeScript UnifiedRuntime):
|
|
20
|
+
#
|
|
21
|
+
# - Config: explicit keyword options first, then the environment. Endpoint from
|
|
22
|
+
# VISIQ_ENDPOINT or VISIQ_BASE_URL (trailing slash stripped); with an API key
|
|
23
|
+
# and no endpoint, the SaaS default DEFAULT_SAAS_ENDPOINT. API key from
|
|
24
|
+
# VISIQ_API_KEY then VISIQ_ALLOW_API_KEY. Agent id from VISIQ_AGENT_ID, else
|
|
25
|
+
# "agent". Fail mode from VISIQ_FAIL_MODE, else "open".
|
|
26
|
+
# - Request: GET {endpoint}/rules/bundle?agent_id=... with Authorization: Bearer,
|
|
27
|
+
# X-VisIQ-SDK: <version> (bare), X-VisIQ-Dialect: 1, and If-None-Match once an
|
|
28
|
+
# ETag is held. A 304 keeps the held bundle. Timeout 2.5 s (connect, and each
|
|
29
|
+
# read).
|
|
30
|
+
# - Refresh: lazy, on the calling thread, before a decision, once the last fetch
|
|
31
|
+
# attempt is older than the refresh interval (5 s). Single flight: one caller
|
|
32
|
+
# fetches; while a bundle is held the others decide on it without waiting, and
|
|
33
|
+
# with no bundle yet they wait for that one fetch and do not start their own.
|
|
34
|
+
# No background thread: a lazy refresh is safe here (the held bundle is one
|
|
35
|
+
# frozen snapshot swapped by a single assignment), and a thread would keep
|
|
36
|
+
# fetching for an agent that is idle and outlive forks the host app makes.
|
|
37
|
+
# - Failure: a failed, timed-out or malformed fetch keeps the last good bundle
|
|
38
|
+
# whatever its age. A held shutdown:true bundle never expires, so a jail stays
|
|
39
|
+
# in force through a control-plane outage and is lifted only by a successful
|
|
40
|
+
# fetch of a non-shutdown bundle.
|
|
41
|
+
# - No bundle (never fetched one, or not configured): fail OPEN by default with
|
|
42
|
+
# one loud "[VisIQ] FAIL-OPEN (no-bundle): ..." stderr line and a non-enforced
|
|
43
|
+
# permit; under fail mode "closed", an enforced deny. A policy deny from a
|
|
44
|
+
# loaded bundle (the jail is one) always applies (G001).
|
|
45
|
+
# - Not configured (no endpoint and key): never touches the network; every
|
|
46
|
+
# decision takes the no-bundle path above.
|
|
47
|
+
#
|
|
48
|
+
# governor = Visiq::Governor.new(agent_id: "support-bot")
|
|
49
|
+
# decision = governor.gate_action(tool_name: "wire_transfer", args: { "amount" => 999 })
|
|
50
|
+
# block_the_call unless decision["allowed"]
|
|
51
|
+
class Governor
|
|
52
|
+
# The managed SaaS control plane, used only when an API key is set and no endpoint is.
|
|
53
|
+
DEFAULT_SAAS_ENDPOINT = "https://api.visiqlabs.com"
|
|
54
|
+
# Seconds the held bundle may age before the next decision re-fetches it.
|
|
55
|
+
DEFAULT_REFRESH_INTERVAL = 5.0
|
|
56
|
+
# Seconds allowed to connect, and for each read, of the bundle request.
|
|
57
|
+
DEFAULT_TIMEOUT = 2.5
|
|
58
|
+
# The wire-contract capability integer sent as X-VisIQ-Dialect (the same value
|
|
59
|
+
# as the Python and TypeScript SDK_DIALECT).
|
|
60
|
+
SDK_DIALECT = 1
|
|
61
|
+
SDK_VERSION_HEADER = "X-VisIQ-SDK"
|
|
62
|
+
SDK_DIALECT_HEADER = "X-VisIQ-Dialect"
|
|
63
|
+
# The X-VisIQ-SDK value this SDK sends: the bare package version, as the
|
|
64
|
+
# Python and TypeScript SDKs send it. The backend reads the header against
|
|
65
|
+
# /^[A-Za-z0-9._+-]+$/ and treats anything else (a "ruby/" prefix included)
|
|
66
|
+
# as absent.
|
|
67
|
+
SDK_HEADER_VALUE = Visiq::VERSION
|
|
68
|
+
|
|
69
|
+
attr_reader :endpoint, :agent_id, :fail_mode, :refresh_interval, :timeout
|
|
70
|
+
|
|
71
|
+
# Every option left nil falls back to the environment. `env:` defaults to ENV
|
|
72
|
+
# and exists so a caller (or a test) can resolve from another source.
|
|
73
|
+
def initialize(endpoint: nil, api_key: nil, agent_id: nil, fail_mode: nil,
|
|
74
|
+
refresh_interval: nil, timeout: nil, env: ENV)
|
|
75
|
+
ep = first_present(endpoint, env["VISIQ_ENDPOINT"], env["VISIQ_BASE_URL"])
|
|
76
|
+
key = first_present(api_key, env["VISIQ_API_KEY"], env["VISIQ_ALLOW_API_KEY"])
|
|
77
|
+
if ep
|
|
78
|
+
ep = ep.sub(%r{/+\z}, "")
|
|
79
|
+
elsif key
|
|
80
|
+
ep = DEFAULT_SAAS_ENDPOINT
|
|
81
|
+
end
|
|
82
|
+
@endpoint = ep
|
|
83
|
+
@api_key = key
|
|
84
|
+
@agent_id = first_present(agent_id, env["VISIQ_AGENT_ID"]) || "agent"
|
|
85
|
+
@fail_mode = Visiq.normalize_fail_mode(fail_mode) || Visiq.normalize_fail_mode(env["VISIQ_FAIL_MODE"]) || "open"
|
|
86
|
+
@refresh_interval = Float(refresh_interval || DEFAULT_REFRESH_INTERVAL)
|
|
87
|
+
@timeout = Float(timeout || DEFAULT_TIMEOUT)
|
|
88
|
+
raise ArgumentError, "refresh_interval must be positive" unless @refresh_interval.positive?
|
|
89
|
+
raise ArgumentError, "timeout must be positive" unless @timeout.positive?
|
|
90
|
+
|
|
91
|
+
@fetch_mutex = Mutex.new
|
|
92
|
+
@held = nil # [frozen bundle Hash, etag String or nil], replaced whole
|
|
93
|
+
@last_attempt = nil # monotonic seconds of the last fetch attempt
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# True when an endpoint and an API key are both present, so it can fetch.
|
|
97
|
+
def configured?
|
|
98
|
+
!@endpoint.nil? && !@api_key.nil?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The (deep-frozen) bundle decisions are made against, or nil before the first
|
|
102
|
+
# successful fetch. Safe to pass on to Visiq.gate_action.
|
|
103
|
+
def bundle
|
|
104
|
+
held = @held
|
|
105
|
+
held && held[0]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Decide one tool/action call for this governor's agent. Same decision shape
|
|
109
|
+
# as Visiq.gate_action: check ["allowed"]; on "mask" apply
|
|
110
|
+
# ["action"]["argRedactionRules"] to the args before running the tool.
|
|
111
|
+
def gate_action(tool_name:, args: nil, target_resource: nil, normalized: nil)
|
|
112
|
+
event = Visiq.action_event(tool_name: tool_name, args: args, agent_id: @agent_id,
|
|
113
|
+
target_resource: target_resource, normalized: normalized)
|
|
114
|
+
decide_event(event, "tool #{tool_name.inspect}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Decide one retrieval for this governor's agent. Same decision shape as
|
|
118
|
+
# Visiq.gate_retrieval: inspect ["retrieval"]["action"].
|
|
119
|
+
def gate_retrieval(resource_metadata: nil, resource_type: "document", query: nil)
|
|
120
|
+
event = Visiq.retrieval_event(resource_metadata: resource_metadata, resource_type: resource_type,
|
|
121
|
+
agent_id: @agent_id, query: query)
|
|
122
|
+
decide_event(event, "retrieval")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Decide any governed event Hash against the held bundle (an "agentId" is
|
|
126
|
+
# added when the event carries none). Same shape as Visiq.decide.
|
|
127
|
+
def decide(event)
|
|
128
|
+
ev = event
|
|
129
|
+
ev = event.merge("agentId" => @agent_id) if event.is_a?(Hash) && event["agentId"].nil?
|
|
130
|
+
decide_event(ev, "event")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def decide_event(event, subject)
|
|
136
|
+
ensure_fresh
|
|
137
|
+
held = @held # ONE consistent snapshot for this decision
|
|
138
|
+
return no_bundle_decision(event, subject) if held.nil?
|
|
139
|
+
|
|
140
|
+
Visiq.decide_with_fail_mode(event, held[0], @fail_mode)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def now
|
|
144
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def recently_attempted?
|
|
148
|
+
last = @last_attempt
|
|
149
|
+
!last.nil? && now - last < @refresh_interval
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Lazy, single-flight refresh on the calling thread.
|
|
153
|
+
def ensure_fresh
|
|
154
|
+
return if !configured? || recently_attempted?
|
|
155
|
+
|
|
156
|
+
if @held
|
|
157
|
+
# A bundle is held: if another caller is already fetching, decide on the
|
|
158
|
+
# held bundle now rather than queue behind (or repeat) its fetch.
|
|
159
|
+
return unless @fetch_mutex.try_lock
|
|
160
|
+
else
|
|
161
|
+
# No bundle yet: wait for the in-flight fetch instead of starting one.
|
|
162
|
+
@fetch_mutex.lock
|
|
163
|
+
end
|
|
164
|
+
begin
|
|
165
|
+
return if recently_attempted? # a caller ahead of us just fetched
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
fetch_once
|
|
169
|
+
ensure
|
|
170
|
+
# Success or failure, the interval restarts here, so an outage costs one
|
|
171
|
+
# timed-out fetch per interval, not one per call.
|
|
172
|
+
@last_attempt = now
|
|
173
|
+
end
|
|
174
|
+
ensure
|
|
175
|
+
@fetch_mutex.unlock
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# One GET /rules/bundle. Any failure keeps the held bundle.
|
|
180
|
+
def fetch_once
|
|
181
|
+
current = @held
|
|
182
|
+
query = URI.encode_www_form_component(@agent_id).gsub("+", "%20")
|
|
183
|
+
uri = URI("#{@endpoint}/rules/bundle?agent_id=#{query}")
|
|
184
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
185
|
+
http.use_ssl = uri.scheme == "https"
|
|
186
|
+
http.open_timeout = @timeout
|
|
187
|
+
http.read_timeout = @timeout
|
|
188
|
+
http.write_timeout = @timeout if http.respond_to?(:write_timeout=)
|
|
189
|
+
http.ssl_timeout = @timeout if http.use_ssl?
|
|
190
|
+
req = Net::HTTP::Get.new(uri)
|
|
191
|
+
req["Accept"] = "application/json"
|
|
192
|
+
req["Authorization"] = "Bearer #{@api_key}"
|
|
193
|
+
req[SDK_VERSION_HEADER] = SDK_HEADER_VALUE
|
|
194
|
+
req[SDK_DIALECT_HEADER] = SDK_DIALECT.to_s
|
|
195
|
+
req["If-None-Match"] = current[1] if current && current[1]
|
|
196
|
+
|
|
197
|
+
res = http.start { |h| h.request(req) }
|
|
198
|
+
code = res.code.to_i
|
|
199
|
+
return if code == 304 && current # unchanged: keep the held bundle
|
|
200
|
+
return fetch_failed("HTTP #{code}") unless code >= 200 && code < 300
|
|
201
|
+
|
|
202
|
+
body = JSON.parse(res.body.to_s)
|
|
203
|
+
return fetch_failed("the bundle response was not a JSON object") unless body.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
@held = [deep_freeze(body), res["ETag"]].freeze
|
|
206
|
+
rescue StandardError => e
|
|
207
|
+
fetch_failed("#{e.class}: #{e.message}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def fetch_failed(detail)
|
|
211
|
+
if @held.nil?
|
|
212
|
+
warn "[VisIQ] bundle fetch failed (ungoverned until loaded): #{detail}"
|
|
213
|
+
else
|
|
214
|
+
warn "[VisIQ] bundle refresh failed (keeping the last good rules): #{detail}"
|
|
215
|
+
end
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def no_bundle_decision(event, subject)
|
|
220
|
+
ops = event.is_a?(Hash) ? Array(event["operations"]) : []
|
|
221
|
+
has_action = ops.include?("action") || ops.include?("delegation")
|
|
222
|
+
has_retrieval = ops.include?("retrieval")
|
|
223
|
+
closed = @fail_mode == "closed"
|
|
224
|
+
unless closed
|
|
225
|
+
warn "[VisIQ] FAIL-OPEN (no-bundle): governance unavailable for #{subject}; " \
|
|
226
|
+
"proceeding UNGOVERNED. Set VISIQ_FAIL_MODE=closed to block instead."
|
|
227
|
+
end
|
|
228
|
+
{
|
|
229
|
+
"decision" => closed ? "deny" : "permit",
|
|
230
|
+
"allowed" => !closed,
|
|
231
|
+
"reason" => if closed
|
|
232
|
+
"no rules bundle loaded (fail-closed, VISIQ_FAIL_MODE=closed)"
|
|
233
|
+
else
|
|
234
|
+
"fail-open: no rules bundle loaded; proceeding UNGOVERNED " \
|
|
235
|
+
"(set VISIQ_FAIL_MODE=closed to block instead)"
|
|
236
|
+
end,
|
|
237
|
+
"ruleId" => nil,
|
|
238
|
+
"ruleCode" => nil,
|
|
239
|
+
"enforced" => closed,
|
|
240
|
+
"action" => if has_action
|
|
241
|
+
{ "decision" => closed ? "deny" : "permit", "allowed" => !closed }
|
|
242
|
+
else
|
|
243
|
+
{ "decision" => nil, "allowed" => true }
|
|
244
|
+
end,
|
|
245
|
+
"retrieval" => { "action" => has_retrieval ? (closed ? "deny" : "allow") : nil }
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def first_present(*values)
|
|
250
|
+
values.find { |v| !v.nil? && !v.to_s.empty? }&.to_s
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def deep_freeze(obj)
|
|
254
|
+
case obj
|
|
255
|
+
when Hash then obj.each_value { |v| deep_freeze(v) }
|
|
256
|
+
when Array then obj.each { |v| deep_freeze(v) }
|
|
257
|
+
end
|
|
258
|
+
obj.freeze
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
data/lib/visiq.rb
CHANGED
|
@@ -43,7 +43,12 @@ module Visiq
|
|
|
43
43
|
# Semantic-Kernel adapter defect (see its Cargo.toml). That hold gates the PyPI
|
|
44
44
|
# wheel, which ships the core's own version; Ruby and Java version independently
|
|
45
45
|
# and are not implicated. Bumping here does not resume the Python release.
|
|
46
|
-
|
|
46
|
+
#
|
|
47
|
+
# 0.1.3 -> 0.1.4 adds Visiq::Governor (lib/visiq/governor.rb), the
|
|
48
|
+
# self-refreshing client that fetches GET /rules/bundle itself, so an agent put
|
|
49
|
+
# in Agent Jail is denied on its next call. Additive: every module function
|
|
50
|
+
# below keeps its signature and behaviour.
|
|
51
|
+
VERSION = "0.1.4"
|
|
47
52
|
|
|
48
53
|
# Low-level binding to the compiled core over the C-ABI seam.
|
|
49
54
|
module Core
|
|
@@ -115,8 +120,13 @@ module Visiq
|
|
|
115
120
|
# VisIQ packaging/outage bug never disrupts the agent; "closed": raise as
|
|
116
121
|
# before. Policy outcomes from a successful evaluation are unaffected.
|
|
117
122
|
def fail_mode
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
normalize_fail_mode(ENV["VISIQ_FAIL_MODE"]) || "open"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# "open" / "closed" (case-insensitive, trimmed), or nil for anything else.
|
|
127
|
+
def normalize_fail_mode(raw)
|
|
128
|
+
v = raw.to_s.strip.downcase
|
|
129
|
+
%w[open closed].include?(v) ? v : nil
|
|
120
130
|
end
|
|
121
131
|
|
|
122
132
|
# A non-enforced permit decision naming the failure cause — the fail-open
|
|
@@ -141,10 +151,17 @@ module Visiq
|
|
|
141
151
|
# Evaluate one governed agent event against a rules bundle. Returns the
|
|
142
152
|
# UnifiedDecision hash: {"decision","allowed","action","retrieval"}.
|
|
143
153
|
def decide(event, bundle)
|
|
154
|
+
decide_with_fail_mode(event, bundle, fail_mode)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# #decide with the fail mode supplied by the caller rather than read from
|
|
158
|
+
# VISIQ_FAIL_MODE. Visiq::Governor uses it so an explicit fail_mode option
|
|
159
|
+
# governs core failures too.
|
|
160
|
+
def decide_with_fail_mode(event, bundle, mode)
|
|
144
161
|
JSON.parse(Core.evaluate_json(JSON.generate(event), JSON.generate(bundle)))
|
|
145
162
|
rescue StandardError, Fiddle::DLError => e
|
|
146
163
|
# Core load / FFI / parse failure — a HARNESS error, never a policy one.
|
|
147
|
-
raise if
|
|
164
|
+
raise if mode == "closed"
|
|
148
165
|
|
|
149
166
|
fail_open_decision(event["operations"] || [], e.message)
|
|
150
167
|
end
|
|
@@ -152,19 +169,33 @@ module Visiq
|
|
|
152
169
|
# Gate a tool/action call. Check ["allowed"]; on "mask" apply
|
|
153
170
|
# ["action"]["argRedactionRules"] to the args before running the tool.
|
|
154
171
|
def gate_action(bundle, tool_name:, args: nil, agent_id: "agent", target_resource: nil, normalized: nil)
|
|
172
|
+
decide(action_event(tool_name: tool_name, args: args, agent_id: agent_id,
|
|
173
|
+
target_resource: target_resource, normalized: normalized), bundle)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The event #gate_action decides; shared with Visiq::Governor.
|
|
177
|
+
def action_event(tool_name:, args: nil, agent_id: "agent", target_resource: nil, normalized: nil)
|
|
155
178
|
event = { "operations" => ["action"], "agentId" => agent_id, "toolName" => tool_name }
|
|
156
179
|
event["toolArgs"] = args unless args.nil?
|
|
157
180
|
event["targetResource"] = target_resource unless target_resource.nil?
|
|
158
181
|
event["normalized"] = normalized unless normalized.nil?
|
|
159
|
-
|
|
182
|
+
event
|
|
160
183
|
end
|
|
161
184
|
|
|
162
185
|
# Gate a retrieval. Inspect ["retrieval"]["action"] — drop on deny/escalate,
|
|
163
186
|
# redact via ["retrieval"]["redactionRules"] — before content reaches the model.
|
|
164
187
|
def gate_retrieval(bundle, resource_metadata: nil, resource_type: "document", agent_id: "agent", query: nil)
|
|
188
|
+
decide(retrieval_event(resource_metadata: resource_metadata, resource_type: resource_type,
|
|
189
|
+
agent_id: agent_id, query: query), bundle)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The event #gate_retrieval decides; shared with Visiq::Governor.
|
|
193
|
+
def retrieval_event(resource_metadata: nil, resource_type: "document", agent_id: "agent", query: nil)
|
|
165
194
|
event = { "operations" => ["retrieval"], "agentId" => agent_id, "resourceType" => resource_type }
|
|
166
195
|
event["resourceMetadata"] = resource_metadata unless resource_metadata.nil?
|
|
167
196
|
event["query"] = query unless query.nil?
|
|
168
|
-
|
|
197
|
+
event
|
|
169
198
|
end
|
|
170
199
|
end
|
|
200
|
+
|
|
201
|
+
require_relative "visiq/governor"
|
data/vendor/libvisiq_core.so
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: visiq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- VisIQ Labs Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: fiddle
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- LICENSE
|
|
40
40
|
- README.md
|
|
41
41
|
- lib/visiq.rb
|
|
42
|
+
- lib/visiq/governor.rb
|
|
42
43
|
- vendor/libvisiq_core.so
|
|
43
44
|
homepage: https://docs.visiqlabs.com
|
|
44
45
|
licenses:
|