@visiq/openclaw-plugin 0.1.4

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.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @visiq/openclaw-plugin
2
+
3
+ VisIQ governance plugin for OpenClaw. Intercepts tool calls and retrievals for real-time policy enforcement.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ openclaw plugins install @visiq/openclaw-plugin
9
+ ```
10
+
11
+ ## Configure
12
+
13
+ ```bash
14
+ export VISIQ_API_KEY=<your-api-key>
15
+ export VISIQ_AGENT_ID=<your-agent-id>
16
+ ```
17
+
18
+ See the [VisIQ documentation](https://docs.visiqlabs.com/quickstart/openclaw) for a full walkthrough.
@@ -0,0 +1,11 @@
1
+ import{readFile as ye}from"node:fs/promises";import{homedir as D}from"node:os";import{join as V,isAbsolute as we,resolve as ve}from"node:path";import{createInterface as Ie}from"node:readline";import{parseArgs as be}from"node:util";var K="@visiq/openclaw-plugin",Ae='[VisIQ] OpenClaw plugin is installed but missing credentials. Provide them with one of (highest precedence first): (1) environment variables VISIQ_API_KEY=<key> VISIQ_AGENT_ID=<id> (set before `openclaw gateway run` / `openclaw start`) ; (2) a JSON file at the path in VISIQ_CONFIG_PATH ; (3) ~/.openclaw/openclaw.json -> plugins.entries["@visiq/openclaw-plugin"].config = { "apiKey": "<key>", "agentId": "<id>" } . Plugin will load but all hooks will no-op until credentials are provided.';function _e(e){return e==="~"||e.startsWith("~/")?V(D(),e.slice(1).replace(/^\//,"")):e}async function W(e){try{let t=we(e)?e:ve(e),n=await ye(t,"utf-8");return JSON.parse(n)}catch{return null}}function k(e,...t){for(let n of t){let r=e[n];if(typeof r=="string")return r}}function P(e){if(!e||typeof e!="object")return{};let t=e,n=k(t,"apiKey","api_key"),r=k(t,"agentId","agent_id"),i=k(t,"baseUrl","base_url"),o={};return n&&(o.apiKey=n),r&&(o.agentId=r),i&&(o.baseUrl=i),o}async function Te(e){if(!e)return{};let t=_e(e),n=await W(t);if(!n||typeof n!="object")return{};let r=n,i=r.plugins?.entries,o=i&&typeof i=="object"?i[K]:void 0;return o&&typeof o=="object"&&o.config?P(o.config):P(r)}async function Re(){let e=V(D(),".openclaw","openclaw.json"),t=await W(e);if(!t||typeof t!="object")return{};let o=t.plugins?.entries?.[K];return!o||typeof o!="object"?{}:P(o.config)}async function Le(e){if(!process.stdout.isTTY)return{};let t=Ie({input:process.stdin,output:process.stdout}),n=r=>new Promise(i=>t.question(r,o=>i(o)));try{let r={...e};if(!r.apiKey){let i=(await n("VisIQ API key: ")).trim();i&&(r.apiKey=i)}if(!r.agentId){let i=(await n("VisIQ agent ID: ")).trim();i&&(r.agentId=i)}if(!r.baseUrl){let i=(await n("VisIQ base URL (optional, press Enter for default): ")).trim();i&&(r.baseUrl=i)}return r}finally{t.close()}}function Ee(){try{let{values:e}=be({args:process.argv.slice(2),options:{"api-key":{type:"string"},"agent-id":{type:"string"},"base-url":{type:"string"}},strict:!1,allowPositionals:!0}),t={},n=e["api-key"],r=e["agent-id"],i=e["base-url"];return typeof n=="string"&&n&&(t.apiKey=n),typeof r=="string"&&r&&(t.agentId=r),typeof i=="string"&&i&&(t.baseUrl=i),t}catch{return{}}}async function F(){let e=Ee(),t={};process.env.VISIQ_API_KEY&&(t.apiKey=process.env.VISIQ_API_KEY),process.env.VISIQ_AGENT_ID&&(t.agentId=process.env.VISIQ_AGENT_ID),process.env.VISIQ_BASE_URL?t.baseUrl=process.env.VISIQ_BASE_URL:process.env.VISIQ_ENDPOINT&&(t.baseUrl=process.env.VISIQ_ENDPOINT);let n=await Te(process.env.VISIQ_CONFIG_PATH),i={...await Re(),...n,...t,...e};if((!i.apiKey||!i.agentId)&&process.stdout.isTTY&&process.stdin.isTTY&&(i=await Le(i)),!i.apiKey||!i.agentId)return process.stderr.write(`${Ae}
2
+ `),null;let o={apiKey:i.apiKey,agentId:i.agentId};return i.baseUrl&&(o.baseUrl=i.baseUrl),o}var M=null,q={ready:!1,config:null};function v(){return M||(M=F().catch(e=>(process.stderr.write(`[VisIQ] Config resolution failed (treating as unconfigured): ${e instanceof Error?e.message:String(e)}
3
+ `),null)).then(e=>(q={ready:!0,config:e},e))),M}function L(){return q}function At(){v()}var x=class{entries=new Map;set(t,n){this.entries.set(t,n)}get(t){return this.entries.get(t)}delete(t){this.entries.delete(t)}clear(){this.entries.clear()}size(){return this.entries.size}},$=new Map;function _(e){let t=$.get(e);return t||(t=new x,$.set(e,t)),t}function B(e){$.delete(e)}var Q=new Set(["web_search","x_search","web_fetch","tool_search","tool_describe","get_account","get_transactions","get_devices","check_watchlist","search_knowledge"]);function Ce(e){if(typeof e!="string"||e.length===0)return e;let t=e.lastIndexOf("__");return t<0?e:e.slice(t+2)}function U(e){return Q.has(Ce(e))}function Rt(){return Q}import{z as g}from"zod";import{z as c}from"zod";import{createHash as Se}from"crypto";var G=new Map;function ke(e){let t=Se("sha256").update(e).digest("hex"),n=G.get(t);if(n)return n;let r=new Map,i=[],o=e.split(`
4
+ `).map(l=>Me(l)).filter(l=>l.trim().length>0),s=0;for(;s<o.length;){let l=o[s].trim();if(l.startsWith("package ")||l.startsWith("import ")){s++;continue}let p=l.match(/^default\s+(\w+)\s*=\s*(.+)$/);if(p){let f=p[1],h=p[2].trim();r.set(f,C(h)),s++;continue}let m=l.match(/^(\w+)\s*=\s*(.+?)\s*\{(.*)$/);if(m){let f=m[1],h=m[2].trim(),y=m[3].trim(),A=[];if(y&&y!=="}"){let j=y.endsWith("}")?y.slice(0,-1).trim():y;j&&A.push(j),y.endsWith("}")||(s++,s=E(o,s,A))}else y==="}"||(s++,s=E(o,s,A));let R=z(A);i.push({variable:f,value:C(h),conditions:R}),s++;continue}let w=l.match(/^(\w+)\s*\{(.*)$/);if(w){let f=w[1],h=w[2].trim(),y=[];if(h&&h!=="}"){let R=h.endsWith("}")?h.slice(0,-1).trim():h;R&&y.push(R),h.endsWith("}")||(s++,s=E(o,s,y))}else h==="}"||(s++,s=E(o,s,y));let A=z(y);i.push({variable:f,value:"true",conditions:A}),s++;continue}s++}let a={defaults:r,rules:i};return G.set(t,a),a}function Pe(e,t){let n={};for(let[r,i]of e.defaults)n[r]=i;for(let r=0;r<e.rules.length;r++){let i=e.rules[r];if(i.conditions.every(o=>$e(o,t)))return n[i.variable]=i.value,{bindings:n,matched:!0,matchedRuleIndex:r}}return{bindings:n,matched:!1,matchedRuleIndex:-1}}function Me(e){let t=!1,n="";for(let r=0;r<e.length;r++){let i=e[r];if(t)i===n&&e[r-1]!=="\\"&&(t=!1);else if(i==='"'||i==="'")t=!0,n=i;else if(i==="#")return e.slice(0,r)}return e}function E(e,t,n){let r=1,i=t;for(;i<e.length&&r>0;){let o=e[i].trim();for(let s of o)s==="{"&&r++,s==="}"&&r--;if(r>0)n.push(o),i++;else{let s=o.slice(0,o.lastIndexOf("}")).trim();return s&&n.push(s),i}}return i}function C(e){return e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'")?e.slice(1,-1):e}function z(e){let t=[];for(let n of e){let r=n.split(";").map(i=>i.trim()).filter(i=>i.length>0);for(let i of r){let o=xe(i);o&&t.push(o)}}return t}function xe(e){let t=!1,n=e.trim();n.startsWith("not ")&&(t=!0,n=n.slice(4).trim());let r=n.match(/^count\(([^)]+)\)\s*>\s*(\d+)$/);if(r)return{type:"count_gt",negated:t,field:r[1].trim(),value:parseInt(r[2],10)};let i=n.match(/^startswith\(([^,]+),\s*"([^"]*)"\)$/);if(i)return{type:"startswith",negated:t,field:i[1].trim(),value:i[2]};let o=n.match(/^endswith\(([^,]+),\s*"([^"]*)"\)$/);if(o)return{type:"endswith",negated:t,field:o[1].trim(),value:o[2]};let s=n.match(/^contains\(([^,]+),\s*"([^"]*)"\)$/);if(s)return{type:"contains",negated:t,field:s[1].trim(),value:s[2]};let a=n.match(/^([^\s=!]+)\s*==\s*(.+)$/);if(a)return{type:"equality",negated:t,field:a[1].trim(),value:C(a[2].trim())};let l=n.match(/^([^\s=!]+)\s*!=\s*(.+)$/);if(l)return{type:"inequality",negated:t,field:l[1].trim(),value:C(l[2].trim())};let p=n.match(/^(input\.[a-zA-Z0-9_.]+)$/);return p?{type:"truthy",negated:t,field:p[1].trim()}:{type:"truthy",negated:!1,field:"__unrecognized_condition_forces_deny__"}}function $e(e,t){let n;switch(e.type){case"equality":{let r=I(e.field,t);n=String(r)===String(e.value);break}case"inequality":{let r=I(e.field,t);n=String(r)!==String(e.value);break}case"startswith":{let r=I(e.field,t);typeof r!="string"?n=!1:n=r.startsWith(String(e.value));break}case"endswith":{let r=I(e.field,t);typeof r!="string"?n=!1:n=r.endsWith(String(e.value));break}case"contains":{let r=I(e.field,t);typeof r!="string"?n=!1:n=r.includes(String(e.value));break}case"truthy":{let r=I(e.field,t);n=Oe(r);break}case"count_gt":{let r=I(e.field,t),i=typeof e.value=="number"?e.value:0;Array.isArray(r)||typeof r=="string"?n=r.length>i:r&&typeof r=="object"?n=Object.keys(r).length>i:n=!1;break}default:n=!1}return e.negated?!n:n}function I(e,t){let n=(e.startsWith("input.")?e.slice(6):e).split("."),r=t;for(let i of n){if(r==null||typeof r!="object")return;r=r[i]}return r}function Oe(e){return!(e==null||e===!1||e===0||e==="")}function Ne(e,t){if(!e||e.trim().length===0)return{decision:"permit",matched:!0};try{let n=ke(e),r=Pe(n,{target_app:t.target_app,action:t.action,...t.context,normalized:t.normalized??{}});if(!r.matched)return{decision:"deny",matched:!1};let i=r.bindings.decision;return i!==void 0?{decision:je(i),matched:!0}:r.bindings.approval_required==="true"?{decision:"approval_required",matched:!0}:r.bindings.allow==="true"?{decision:"permit",matched:!0}:{decision:"deny",matched:!0}}catch{return{decision:"deny",matched:!1}}}function je(e){switch(e){case"permit":case"allow":return"permit";case"deny":return"deny";case"approval_required":return"approval_required";default:return"deny"}}function De(e,t){if(!e||e==="*")return!0;if(e.endsWith("*")){let n=e.slice(0,-1);return t.startsWith(n)}return e===t}function H(e,t){let n=[...e].sort((r,i)=>i.priority-r.priority);for(let r of n){if(r.target_app!==null&&r.target_app!==t.target_app||!De(r.action_pattern,t.action))continue;let i=Ne(r.rego_source,t);if(i.matched)return{decision:i.decision,reason:r.description??r.name,rule_id:r.id,rule_code:r.rule_code,description:r.description,matchedRule:!0}}return{decision:"deny",reason:"No matching rule",rule_id:null,rule_code:null,description:null,matchedRule:!1}}var Ve=g.object({agentId:g.string().min(1,"agentId is required"),toolName:g.string().min(1,"toolName is required"),toolArgs:g.record(g.unknown()),targetResource:g.string().optional(),context:g.record(g.unknown()).optional(),telemetry:g.record(g.unknown()).optional()}),kt=g.object({allowed:g.boolean(),reason:g.string(),ruleId:g.string().optional(),ruleCode:g.string().optional(),description:g.string().optional(),enforced:g.boolean().optional(),decision:g.enum(["permit","deny","approval_required"]).optional(),decisionId:g.string().optional()});var Ke=c.enum(["enforce","audit","off"]),We=c.enum(["closed","open"]),Z=c.enum(["enforce","monitor","off"]),Mt=c.object({apiKey:c.string().min(1,"apiKey is required"),agentId:c.string().min(1,"agentId is required"),endpoint:c.string().url().optional(),mode:Ke.optional(),timeoutMs:c.number().int().positive().optional(),hitlTimeoutMs:c.number().int().positive().optional(),failBehavior:We.optional()}),xt=c.object({agent_id:c.string().min(1),target_app:c.string().min(1),action:c.string().min(1),context:c.record(c.unknown()).optional(),telemetry:c.record(c.unknown()).optional()}),Fe=c.object({decision_id:c.string().min(1),decision:c.enum(["permit","deny","approval_required"]),reason:c.string().optional(),rule_code:c.string().nullable().optional(),enforced:c.boolean().optional(),agent_mode:Z.optional(),metadata:c.record(c.unknown()).optional()}),qe=c.object({field:c.string().min(1),operator:c.enum(["equals","in","not_equals","not_in","glob","gt","lt","gte","lte","contains","not_contains"]),value:c.union([c.string(),c.number(),c.array(c.string())])}),Be=c.object({id:c.string().min(1),rule_code:c.string().default(""),name:c.string().min(1),description:c.string().nullable().default(null),effect:c.enum(["allow","deny","hitl"]),resource_type:c.string().min(1),rego_source:c.string().default(""),target_app:c.string().nullable().default(null),action_pattern:c.string().nullable().default(null),conditions:c.array(qe).default([]),priority:c.number().int().default(0)}),Qe=c.object({version:c.string().min(1),agent_mode:Z.default("monitor"),rules:c.array(Be)}),Ue=2e3,Ge=class{config;originalFetch;constructor(e,t){this.config=e,this.originalFetch=t}async evaluate(e){let t=new AbortController,n=setTimeout(()=>t.abort(),this.config.timeoutMs),r;try{r=await this.originalFetch(`${this.config.endpoint}/allow/evaluate`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(e),signal:t.signal})}finally{clearTimeout(n)}if(!r.ok)throw new Error(`ALLOW evaluate request failed: HTTP ${r.status} ${r.statusText}`);let i=await r.json();return Fe.parse(i)}async fetchBundle(){let e=this.config.agentId?`?agent_id=${encodeURIComponent(this.config.agentId)}`:"",t=`${this.config.endpoint}/allow/rules/bundle${e}`,n=new AbortController,r=setTimeout(()=>n.abort(),this.config.timeoutMs),i;try{i=await this.originalFetch(t,{method:"GET",headers:{Authorization:`Bearer ${this.config.apiKey}`,Accept:"application/json"},signal:n.signal})}finally{clearTimeout(r)}if(!i.ok)throw new Error(`ALLOW rules/bundle fetch failed: HTTP ${i.status} ${i.statusText}`);let o=await i.json();return Qe.parse(o)}async sendTelemetry(e){if(e.length===0)return;let t=`${this.config.endpoint}/allow/telemetry`;try{let n=await this.originalFetch(t,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({decisions:e})});n.ok||console.warn(`[ALLOW] Telemetry delivery failed: HTTP ${n.status} ${n.statusText}`)}catch(n){console.warn("[ALLOW] Telemetry delivery failed:",n)}}async reportExecutionResult(e,t,n){let r=`${this.config.endpoint}/allow/execution-events`;try{let i=await this.originalFetch(r,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({decision_id:e,result:t,details:n})});i.ok||console.warn(`[ALLOW] Execution result report failed: HTTP ${i.status}`)}catch(i){console.warn("[ALLOW] Execution result report failed:",i)}}async waitForApproval(e){if(!e)throw new Error("waitForApproval: decisionId must be a non-empty string");let t=Date.now()+this.config.hitlTimeoutMs;for(;Date.now()<t;){let n=new AbortController,r=setTimeout(()=>n.abort(),this.config.timeoutMs),i;try{i=await this.originalFetch(`${this.config.endpoint}/v1/allow/decisions/${encodeURIComponent(e)}`,{method:"GET",headers:{Authorization:`Bearer ${this.config.apiKey}`},signal:n.signal})}finally{clearTimeout(r)}if(!i.ok)throw new Error(`ALLOW decisions poll failed: HTTP ${i.status} ${i.statusText}`);let o=await i.json();if(typeof o!="object"||o===null)throw new Error("ALLOW decisions poll returned unexpected non-object response");let s=o,a=s.hitl_result;if(a==="approved")return{decision_id:e,decision:"permit",reason:"Approved by human reviewer"};if(a==="rejected")return{decision_id:e,decision:"deny",reason:typeof s.reason=="string"?s.reason:"Rejected by human reviewer"};if(a==="timeout"||a==="expired")return{decision_id:e,decision:"deny",reason:"HITL approval timed out"};await ze(Ue)}return{decision_id:e,decision:"deny",reason:"HITL approval timed out"}}};function ze(e){return new Promise(t=>setTimeout(t,e))}var He=class{endpoint;apiKey;agentId;originalFetch;onModeChanged;abortController=null;reconnectTimer=null;backoffMs=1e3;stableTimer=null;stopped=!1;constructor(e){this.endpoint=e.endpoint,this.apiKey=e.apiKey,this.agentId=e.agentId,this.originalFetch=e.originalFetch,this.onModeChanged=e.onModeChanged}start(){if(this.stopped)throw new Error("AgentModeStream cannot be restarted after stop()");this.connect()}stop(){this.stopped=!0,this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.stableTimer!==null&&(clearTimeout(this.stableTimer),this.stableTimer=null),this.abortController!==null&&(this.abortController.abort(),this.abortController=null)}async connect(){if(this.stopped)return;let e=new AbortController;this.abortController=e;let t=`${this.endpoint}/allow/agents/me/stream?agent_id=${encodeURIComponent(this.agentId)}`,n;try{n=await this.originalFetch(t,{method:"GET",headers:{Authorization:`Bearer ${this.apiKey}`,Accept:"text/event-stream","Cache-Control":"no-cache"},signal:e.signal})}catch(r){this.scheduleReconnect(`connection failed: ${Y(r)}`);return}if(!n.ok){this.scheduleReconnect(`HTTP ${n.status} ${n.statusText}`);return}if(!n.body){this.scheduleReconnect("no response body");return}this.stableTimer=setTimeout(()=>{this.backoffMs=1e3,this.stableTimer=null},3e4);try{await this.readLoop(n.body.getReader())}catch(r){if(this.stopped)return;this.scheduleReconnect(`read loop failed: ${Y(r)}`);return}this.stopped||this.scheduleReconnect("stream ended")}async readLoop(e){let t=new TextDecoder("utf-8"),n="";for(;;){let{value:r,done:i}=await e.read();if(i)break;if(this.stopped)return;n+=t.decode(r,{stream:!0});let o;for(;(o=n.indexOf(`
5
+
6
+ `))!==-1;){let s=n.slice(0,o);n=n.slice(o+2),this.handleEvent(s)}}}handleEvent(e){let t="message",n=[];for(let o of e.split(`
7
+ `)){if(o===""||o.startsWith(":"))continue;let s=o.indexOf(":");if(s===-1)continue;let a=o.slice(0,s),l=o.slice(s+1);l.startsWith(" ")&&(l=l.slice(1)),a==="event"?t=l:a==="data"&&n.push(l)}if(n.length===0||t!=="mode_changed"&&t!=="mode_snapshot")return;let r;try{r=JSON.parse(n.join(`
8
+ `))}catch{return}if(r===null||typeof r!="object"||!("agent_mode"in r))return;let i=r.agent_mode;if(!(i!=="enforce"&&i!=="monitor"&&i!=="off"))try{this.onModeChanged(i)}catch(o){console.error("[ALLOW] mode_changed listener threw:",o)}}scheduleReconnect(e){if(this.stopped)return;this.stableTimer!==null&&(clearTimeout(this.stableTimer),this.stableTimer=null),this.abortController!==null&&(this.abortController=null);let t=this.backoffMs;this.backoffMs=Math.min(this.backoffMs*2,3e4),console.warn(`[ALLOW] mode stream disconnected (${e}); reconnecting in ${t}ms`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},t),this.reconnectTimer!==null&&typeof this.reconnectTimer.unref=="function"&&this.reconnectTimer.unref()}};function Y(e){return e instanceof Error?e.message:String(e)}var Ye=5e3,Je=1e4,Ze="monitor",X=class{client;resolvedApiKey;resolvedEndpoint;resolvedAgentId;deferHitl;rules=[];bundleVersion=null;bundleFetchInProgress=!1;refreshTimer=null;telemetryBuffer=[];telemetryTimer=null;agentMode=Ze;modeStream=null;constructor(e,t,n,r){let i=e??process.env.ALLOW_API_KEY,o=t??process.env.ALLOW_ENDPOINT,s=n??process.env.ALLOW_AGENT_ID??process.env.VISIQ_AGENT_ID??"";if(this.resolvedApiKey=i,this.resolvedEndpoint=o,this.resolvedAgentId=s,this.deferHitl=r?.deferHitl??!1,i&&o){let a={apiKey:i,agentId:s,endpoint:o,mode:"enforce",timeoutMs:5e3,hitlTimeoutMs:3e5,failBehavior:"closed"},l=globalThis.fetch.bind(globalThis);this.client=new Ge(a,l),this.fetchBundleBackground(),this.refreshTimer=setInterval(()=>this.fetchBundleBackground(),Ye),J(this.refreshTimer),this.telemetryTimer=setInterval(()=>this.flushTelemetry(),Je),J(this.telemetryTimer),s&&(this.modeStream=new He({endpoint:o,apiKey:i,agentId:s,originalFetch:l,onModeChanged:p=>{this.agentMode=p}}),this.modeStream.start())}else this.client=null}async evaluate(e){let t=Ve.parse(e),n=Date.now(),r=this.agentMode;if(r==="off")return{allowed:!0,reason:"agent_mode=off \u2014 evaluation bypassed",enforced:!1};if(this.rules.length>0){let i=H(this.toMatchable(),this.toMatchInput(t));if(i.matchedRule){if(i.decision==="approval_required"){if(this.client){let s=await this.evaluateRemote(t,n);return this.applyAgentMode(s,r,t,n,"remote")}return this.applyAgentMode({allowed:!1,reason:`${i.reason} (human approval required \u2014 no backend configured)`,...i.rule_id?{ruleId:i.rule_id}:{},...i.rule_code?{ruleCode:i.rule_code}:{},...i.description?{description:i.description}:{}},r,t,n,"local")}let o={allowed:i.decision==="permit",reason:i.reason,...i.rule_id?{ruleId:i.rule_id}:{},...i.rule_code?{ruleCode:i.rule_code}:{},...i.description?{description:i.description}:{}};return this.applyAgentMode(o,r,t,n,"local")}if(this.client){let o=await this.evaluateRemote(t,n);return this.applyAgentMode(o,r,t,n,"remote")}return this.applyAgentMode({allowed:!1,reason:"No matching ALLOW rule and no backend configured \u2014 fail-closed (G001)"},r,t,n,"local")}if(this.client){let i=await this.evaluateRemote(t,n);return this.applyAgentMode(i,r,t,n,"remote")}return this.applyAgentMode({allowed:!1,reason:"No ALLOW rules loaded and no backend configured \u2014 fail-closed (G001)"},r,t,n,"local")}hasRules(){return this.rules.length>0}hasClient(){return this.client!==null}getBundleVersion(){return this.bundleVersion}getAgentMode(){return this.agentMode}getEndpoint(){return this.resolvedEndpoint}getApiKey(){return this.resolvedApiKey}dispose(){this.refreshTimer&&(clearInterval(this.refreshTimer),this.refreshTimer=null),this.telemetryTimer&&(clearInterval(this.telemetryTimer),this.telemetryTimer=null),this.modeStream&&(this.modeStream.stop(),this.modeStream=null),this.flushTelemetry().catch(e=>{console.error("[AllowRuntime] final telemetry flush failed during destroy (non-fatal):",e)})}applyAgentMode(e,t,n,r,i){let o=Date.now()-r;return t==="enforce"?(this.bufferTelemetry(n,e,o,i,!0),{...e,enforced:!0}):(this.bufferTelemetry(n,e,o,i,!1),{allowed:!0,reason:e.reason,...e.ruleId!==void 0?{ruleId:e.ruleId}:{},...e.ruleCode!==void 0?{ruleCode:e.ruleCode}:{},...e.description!==void 0?{description:e.description}:{},enforced:!1})}toMatchable(){return this.rules.map(e=>({id:e.id,rule_code:e.rule_code||null,name:e.name,description:e.description,rego_source:e.rego_source??null,target_app:e.target_app??null,action_pattern:e.action_pattern??null,priority:e.priority}))}toMatchInput(e){return{target_app:e.targetResource??e.toolName,action:e.toolName,context:{[e.toolName]:e.toolArgs,...e.context??{}},normalized:{}}}async evaluateRemote(e,t){if(!this.client)return{allowed:!1,reason:"No backend configured \u2014 fail-closed (G001)"};try{let n={agent_id:e.agentId,target_app:e.targetResource??e.toolName,action:e.toolName,context:{[e.toolName]:e.toolArgs,...e.context},telemetry:e.telemetry},r=await this.client.evaluate(n);if(r.decision==="approval_required"&&r.decision_id){if(this.deferHitl)return{allowed:!1,reason:r.reason??"Human approval required",decision:"approval_required",decisionId:r.decision_id,...r.rule_code?{ruleCode:r.rule_code}:{}};let i=await this.client.waitForApproval(r.decision_id);return{allowed:i.decision==="permit",reason:i.reason??"HITL decision",...r.rule_code?{ruleCode:r.rule_code}:{}}}return{allowed:r.decision==="permit",reason:r.reason??`Backend: ${r.decision}`,...r.rule_code?{ruleCode:r.rule_code}:{}}}catch(n){return console.warn("[ALLOW] Network evaluation failed \u2014 denying (G001):",n),{allowed:!1,reason:`Network evaluation failed \u2014 fail-closed (G001) [${Date.now()-t}ms]`}}}async fetchBundleBackground(){if(!(this.bundleFetchInProgress||!this.client)){this.bundleFetchInProgress=!0;try{let e=await this.client.fetchBundle();e.version!==this.bundleVersion&&(this.rules=e.rules,this.bundleVersion=e.version),this.agentMode=e.agent_mode}catch(e){this.bundleVersion?console.warn("[ALLOW] Bundle refresh failed (using cached rules):",e):console.warn("[ALLOW] Initial bundle fetch failed (will use network evaluation):",e)}finally{this.bundleFetchInProgress=!1}}}bufferTelemetry(e,t,n,r,i){if(r==="remote")return;let o={agent_id:e.agentId,tool_name:e.toolName,target_resource:e.targetResource,...O(e.toolArgs,e.toolName),...O(e.context??{})};this.telemetryBuffer.push({decision_id:crypto.randomUUID(),agent_id:e.agentId,target_app:e.targetResource??e.toolName,action:e.toolName,decision:t.allowed?"permit":"deny",reason:t.reason,evaluated_at:new Date().toISOString(),rule_id:t.ruleId??null,rule_code:t.ruleCode??null,enforced:i,agent_mode:this.agentMode,latency_ms:n,evaluation_mode:r,raw_event:o})}async flushTelemetry(){if(this.telemetryBuffer.length===0||!this.client)return;let e=this.telemetryBuffer.splice(0);try{await this.client.sendTelemetry(e)}catch(t){console.error("[AllowRuntime] telemetry flush failed (non-fatal):",t)}}};function O(e,t=""){let n={};for(let[r,i]of Object.entries(e)){let o=t?`${t}/${r}`:r;i!==null&&typeof i=="object"&&!Array.isArray(i)?Object.assign(n,O(i,o)):n[o]=i}return n}function J(e){e&&typeof e=="object"&&"unref"in e&&e.unref()}import{z as u}from"zod";import{z as d}from"zod";var Xe=u.enum(["enforce","audit","off"]),et=u.enum(["allow","redact","deny","escalate"]),ee=u.object({agentId:u.string().min(1,"agentId is required"),agentRole:u.string().optional(),trustTier:u.number().int().min(1).max(3).optional(),operation:u.enum(["retrieve","tool_call","prompt_render"]),resourceType:u.enum(["document","tool_result","prompt"]),resourceMetadata:u.record(u.unknown()),query:u.string().optional(),contentPreview:u.string().optional(),telemetry:u.record(u.unknown()).optional()}),Kt=u.object({action:et,reason:u.string(),ruleId:u.string().optional(),redactionRules:u.array(u.object({field:u.string(),pattern:u.string(),replacement:u.string()})).optional(),metadata:u.record(u.unknown()).optional()}),tt=u.object({apiKey:u.string().optional(),endpoint:u.string().url().optional(),mode:Xe.optional(),wasmPath:u.string().optional(),bundleUrl:u.string().url().optional(),syncIntervalMs:u.number().int().positive().optional(),telemetryEnabled:u.boolean().optional(),timeoutMs:u.number().int().positive().optional()}),te="https://project-y-backend-mnxz.onrender.com",ne="enforce",re=6e4,ie=!0,oe=5e3;function ce(e){let t={apiKey:e.apiKey||process.env.RECALL_API_KEY||"",endpoint:e.endpoint||process.env.RECALL_ENDPOINT||te,mode:e.mode||nt(process.env.RECALL_MODE)||ne,wasmPath:e.wasmPath||process.env.RECALL_WASM_PATH,bundleUrl:e.bundleUrl||process.env.RECALL_BUNDLE_URL,syncIntervalMs:e.syncIntervalMs!==void 0?e.syncIntervalMs:se(process.env.RECALL_SYNC_INTERVAL_MS)??re,telemetryEnabled:e.telemetryEnabled!==void 0?e.telemetryEnabled:rt(process.env.RECALL_TELEMETRY_ENABLED)??ie,timeoutMs:e.timeoutMs!==void 0?e.timeoutMs:se(process.env.RECALL_TIMEOUT_MS)??oe},n=tt.parse(t);return{apiKey:n.apiKey??"",endpoint:n.endpoint??te,mode:n.mode??ne,wasmPath:n.wasmPath,bundleUrl:n.bundleUrl,syncIntervalMs:n.syncIntervalMs??re,telemetryEnabled:n.telemetryEnabled??ie,timeoutMs:n.timeoutMs??oe}}function nt(e){if(e==="enforce"||e==="audit"||e==="off")return e;if(e!==void 0)throw new Error(`Invalid RECALL_MODE env var: "${e}". Must be one of: enforce, audit, off`)}function se(e){if(e===void 0)return;let t=parseInt(e,10);if(isNaN(t)||t<=0)throw new Error(`Invalid numeric env var value: "${e}". Must be a positive integer.`);return t}function rt(e){if(e===void 0)return;let t=e.toLowerCase().trim();return t==="true"||t==="1"||t==="yes"}function it(e,t){let n=e;for(let r of t)try{let i=ot(r.pattern);n=n.replace(i,r.replacement)}catch(i){console.warn("[RECALL] redactText: skipping malformed redaction rule:",i)}return n}function ot(e){if(e instanceof RegExp){let n=e.flags.includes("g")?e.flags:`${e.flags}g`;return new RegExp(e.source,n)}let t=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(t,"g")}var N={action:"deny",reason:"No RECALL policy loaded \u2014 fail-closed (G001)"},b={action:"deny",reason:"RECALL policy evaluation error \u2014 fail-closed (G001)"},ue=class{config;client;telemetry;policy=null;policyVersion=null;policyData={};constructor(e,t=null,n=null){this.config=e,this.client=t,this.telemetry=n}async loadBundle(e,t,n){let{loadPolicy:r}=await import("@open-policy-agent/opa-wasm"),i=await r(e);i.setData(t),this.policy=i,this.policyData=t,this.policyVersion=n??null}evaluate(e){let t=ee.parse(e);if(this.policy===null)return N;let n=Date.now();try{let r=ae(t),i=this.policy.evaluate(r),o=le(i);return this.recordTelemetry(t,o,Date.now()-n,"wasm"),o}catch(r){return console.warn("[RECALL] WASM policy evaluation error \u2014 denying (G001):",r),this.recordTelemetry(t,b,Date.now()-n,"wasm"),b}}async evaluateAsync(e){if(this.config.mode==="off")return{action:"allow",reason:"RECALL mode is off"};let t=ee.parse(e),n=Date.now();if(this.policy!==null)try{let r=ae(t),i=this.policy.evaluate(r),o=le(i);return this.recordTelemetry(t,o,Date.now()-n,"wasm"),this.config.mode==="audit"?{action:"allow",reason:`audit: would have ${o.action}`}:o}catch(r){console.warn("[RECALL] WASM evaluation error, falling back to HTTP:",r)}if(this.client!==null)try{let r=await this.client.evaluate(t);return this.recordTelemetry(t,r,Date.now()-n,"remote"),this.config.mode==="audit"?{action:"allow",reason:`audit: would have ${r.action}`}:r}catch(r){return console.warn("[RECALL] Server-side evaluation failed \u2014 denying (G001):",r),this.recordTelemetry(t,b,Date.now()-n,"remote"),b}return this.recordTelemetry(t,N,Date.now()-n,"wasm"),N}async filterDocuments(e,t,n){if(this.config.mode==="off")return t;if(!e||e.trim()==="")return console.warn("[RECALL] filterDocuments called with empty agentId \u2014 denying all (G001)"),[];let r=[];for(let i of t){let o;try{o=n(i)}catch(l){console.warn("[RECALL] extractMeta threw \u2014 excluding document (G001):",l);continue}let s={agentId:e,operation:"retrieve",resourceType:"document",resourceMetadata:o},a;try{a=await this.evaluateAsync(s)}catch(l){console.warn("[RECALL] Unexpected error in evaluateAsync \u2014 excluding document (G001):",l);continue}switch(a.action){case"allow":case"escalate":r.push(i);break;case"redact":{if(a.redactionRules&&a.redactionRules.length>0){let l=st(i,a.redactionRules);r.push(l)}else r.push(i);break}case"deny":break;default:{let l=a.action;break}}}return r}getPolicyVersion(){return this.policyVersion}hasPolicy(){return this.policy!==null}recordTelemetry(e,t,n,r){if(this.telemetry!==null)try{this.telemetry.record({agentId:e.agentId,evaluatedAt:new Date().toISOString(),input:e,decision:t,latencyMs:n,evaluationMode:r})}catch{}}};function ae(e){return{agent_id:e.agentId,agent_role:e.agentRole,trust_tier:e.trustTier,operation:e.operation,resource_type:e.resourceType,resource_metadata:e.resourceMetadata,query:e.query,content_preview:e.contentPreview}}function le(e){if(!Array.isArray(e)||e.length===0)return b;let t=e[0]?.result;if(t===null||typeof t!="object")return b;let n=t,{action:r}=n;if(r!=="allow"&&r!=="redact"&&r!=="deny"&&r!=="escalate")return b;let i=typeof n.reason=="string"?n.reason:"Policy decision",o=typeof n.rule_id=="string"?n.rule_id:void 0,s;if(Array.isArray(n.redaction_rules)){s=[];for(let a of n.redaction_rules)if(a!==null&&typeof a=="object"&&typeof a.field=="string"&&typeof a.pattern=="string"&&typeof a.replacement=="string"){let l=a;s.push({field:l.field,pattern:l.pattern,replacement:l.replacement})}}return{action:r,reason:i,ruleId:o,redactionRules:s}}function st(e,t){if(e===null||typeof e!="object")return e;let n={...e};for(let r of t){let i=n[r.field];typeof i=="string"&&(n[r.field]=it(i,[{pattern:r.pattern,replacement:r.replacement}]))}return n}var at=d.object({decision_id:d.string().min(1),decision:d.enum(["allow","redact","deny","escalate"]),reason_code:d.string().optional(),reason:d.string().optional(),receipt_id:d.string().optional(),rule_id:d.string().optional(),redaction_rules:d.array(d.object({field:d.string().optional(),pattern:d.string().optional(),replacement:d.string().optional()})).optional()}),lt=d.object({id:d.string().min(1),name:d.string().min(1),rego_source:d.string().min(1),trust_tier:d.string().nullable(),surface:d.string().nullable(),principal_exclusions:d.array(d.string()),priority:d.number()}),ct=d.object({version:d.string().min(1),rules:d.array(lt)}),de=class{config;constructor(e){this.config=e}async evaluate(e){let t=new AbortController,n=setTimeout(()=>t.abort(),this.config.timeoutMs),r=e.trustTier!==void 0?`tier${e.trustTier}`:void 0,i={agent_id:e.agentId,operation:e.operation,resource_type:e.resourceType,resource_metadata:e.resourceMetadata,trust_tier:r,surface:e.resourceMetadata?.surface,query:e.query,telemetry:e.telemetry},o;try{o=await fetch(`${this.config.endpoint}/recall/evaluate`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(i),signal:t.signal})}finally{clearTimeout(n)}if(!o.ok)throw new Error(`RECALL evaluate request failed: HTTP ${o.status} ${o.statusText}`);let s=await o.json(),a=at.parse(s),l=a.redaction_rules?.map(p=>({field:p.field??"",pattern:p.pattern??"",replacement:p.replacement??"[REDACTED]"}));return{action:a.decision,reason:a.reason??a.reason_code??"",ruleId:a.rule_id,...l?{redactionRules:l}:{}}}async fetchBundle(){let e=new AbortController,t=setTimeout(()=>e.abort(),this.config.timeoutMs),n;try{n=await fetch(`${this.config.endpoint}/recall/rules/bundle`,{method:"GET",headers:{Authorization:`Bearer ${this.config.apiKey}`,Accept:"application/json"},signal:e.signal})}finally{clearTimeout(t)}if(!n.ok)throw new Error(`RECALL bundle fetch failed: HTTP ${n.status} ${n.statusText}`);let r=await n.json(),i=ct.parse(r);return{rules:i.rules,version:i.version}}async sendTelemetry(e){if(e.length===0)return;let t=`${this.config.endpoint}/recall/telemetry`;try{let n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({events:e})});n.ok||console.warn(`[RECALL] Telemetry delivery failed: HTTP ${n.status} ${n.statusText}`)}catch(n){console.warn("[RECALL] Telemetry delivery failed:",n)}}};var T=null;function pe(e){let t=`${e.apiKey}::${e.agentId}::${e.baseUrl??""}`;if(T&&T.key===t)return T.runtimes;let n=new X(e.apiKey,e.baseUrl,e.agentId),r=ce({apiKey:e.apiKey,endpoint:e.baseUrl}),i=new de(r),o=new ue(r,i,null);return T={key:t,runtimes:{allow:n,recall:o}},T.runtimes}var fe="@visiq/openclaw-plugin";function ut(e){return typeof e=="string"&&e.includes("human approval required")}function dt(e){let t=[],n=[],r;for(let o of e){let{field:s,pattern:a,replacement:l}=o;s&&t.push(s),a&&n.push(a),l&&r===void 0&&(r=l)}let i={};return t.length>0&&(i.fields=t),n.length>0&&(i.patterns=n),r!==void 0&&(i.replacement=r),i}async function Zt(e,t){let n=await v();if(!n)return;let r=U(e.toolName),i=pe(n),o=n.agentId??t.agentId,s=t.sessionId??t.sessionKey??"default",a=t.toolCallId??e.toolCallId,l,p,m;try{({allowed:l,reason:p,ruleCode:m}=await i.allow.evaluate({agentId:o,toolName:e.toolName,toolArgs:e.params??{}}))}catch(f){return{block:!0,blockReason:`[VisIQ ALLOW] Evaluation error (fail-closed): ${f instanceof Error?f.message:String(f)}`}}if(!l)return ut(p)?{requireApproval:{title:`VisIQ approval required: ${e.toolName}`,description:p,severity:"critical",allowedDecisions:["allow-once","allow-always","deny"],pluginId:fe}}:{block:!0,blockReason:`[VisIQ ${m??"DENY"}] ${p}`};if(!r)return;let w;try{w=await i.recall.evaluateAsync({agentId:o,operation:"tool_call",resourceType:"tool_result",resourceMetadata:{toolName:e.toolName,runId:e.runId??t.runId},query:pt(e.params)})}catch(f){return{block:!0,blockReason:`[VisIQ RECALL] Evaluation error (fail-closed): ${f instanceof Error?f.message:String(f)}`}}switch(w.action){case"allow":return;case"deny":return{block:!0,blockReason:`[VisIQ RECALL] ${w.reason}`};case"redact":{if(!a)return{block:!0,blockReason:"[VisIQ RECALL] Redact decision but no toolCallId \u2014 fail-closed"};let f=w.redactionRules??[];if(f.length===0)return;let h=dt(f);_(s).set(a,h);return}case"escalate":return{requireApproval:{title:`VisIQ approval required: ${e.toolName}`,description:w.reason,severity:"warning",allowedDecisions:["allow-once","allow-always","deny"],pluginId:fe}};default:{let{action:f}=w;return{block:!0,blockReason:`[VisIQ RECALL] Unknown action "${String(f)}" \u2014 fail-closed`}}}}function pt(e){if(e)for(let t of["query","q","url","prompt"]){let n=e[t];if(typeof n=="string"&&n.length>0)return n}}var ge="[REDACTED]";function he(e,t){return e==null?e:typeof e=="string"?me(e,t):typeof e=="object"?ft(e,t):e}function me(e,t){if(typeof e!="string")return e;let{patterns:n}=t;if(!n||n.length===0)return e;let r=t.replacement??ge,i=e;for(let o of n){if(typeof o!="string"||o.length===0)continue;let s;try{s=new RegExp(o,"g")}catch(a){let l=a instanceof Error?a.message:String(a);throw new Error(`[visiq-redact] Invalid redaction pattern "${o}": ${l}`)}i=i.replace(s,r)}return i}function ft(e,t){let n=new Set(t.fields??[]),r=t.replacement??ge,i=n.size>0,o=(t.patterns??[]).length>0,s=a=>{if(a==null)return a;if(Array.isArray(a))return a.map(l=>s(l));if(typeof a=="object"){let l=a,p={};for(let m of Object.keys(l)){if(i&&n.has(m)){p[m]=r;continue}p[m]=s(l[m])}return p}return typeof a=="string"&&o?me(a,t):a};return s(e)}function on(e,t){let{ready:n,config:r}=L();if(!n||!r)return;let i=e.toolCallId??t.toolCallId;if(!i)return;let o=t.sessionKey??"default",s=_(o),a=s.get(i);if(!a)return;let l={...e.message};return l.content!==void 0&&(l.content=he(l.content,a)),s.delete(i),{message:l}}function cn(e,t){let{ready:n,config:r}=L();if(n&&!r)return;let i=e.sessionKey??t.sessionKey??"default",o=_(i);if(o.size()>0)return o.clear(),{block:!0}}async function S(e,t){let r=`${(e.baseUrl??"https://api.visiqlabs.com").replace(/\/$/,"")}/v1/events`;try{let i=new AbortController,o=setTimeout(()=>i.abort(),5e3);try{let s=await fetch(r,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${e.apiKey}`},body:JSON.stringify(t),signal:i.signal});s.ok||process.stderr.write(`[VisIQ] /v1/events POST failed: ${s.status} ${s.statusText}
9
+ `);try{await s.text()}catch{}}finally{clearTimeout(o)}}catch(i){if(i instanceof Error&&i.name==="AbortError"){process.stderr.write(`[VisIQ] /v1/events POST timed out (5s)
10
+ `);return}process.stderr.write(`[VisIQ] /v1/events POST error: ${i instanceof Error?i.message:String(i)}
11
+ `)}}async function gn(e,t){let n=await v();if(!n)return;let r=n.agentId??t.agentId;S(n,{type:"llm_input",agentId:r,sessionId:e.sessionId??t.sessionId??t.sessionKey,runId:e.runId??t.runId,timestamp:new Date().toISOString(),payload:{provider:e.provider,model:e.model,hasSystemPrompt:typeof e.systemPrompt=="string"&&e.systemPrompt.length>0,promptLength:typeof e.prompt=="string"?e.prompt.length:0,historyMessageCount:Array.isArray(e.historyMessages)?e.historyMessages.length:0,imagesCount:e.imagesCount??0,toolCount:Array.isArray(e.tools)?e.tools.length:0}})}function gt(e){if(!Array.isArray(e))return 0;let t=0;for(let n of e)typeof n=="string"&&(t+=n.length);return t}async function wn(e,t){let n=await v();if(!n)return;let r=n.agentId??t.agentId;S(n,{type:"llm_output",agentId:r,sessionId:e.sessionId??t.sessionId??t.sessionKey,runId:e.runId??t.runId,timestamp:new Date().toISOString(),payload:{provider:e.provider,model:e.model,resolvedRef:e.resolvedRef,harnessId:e.harnessId,assistantSegmentCount:Array.isArray(e.assistantTexts)?e.assistantTexts.length:0,assistantTotalChars:gt(e.assistantTexts),usage:e.usage??null}})}function bn(e,t){B(e.sessionId??e.sessionKey??"default")}export{F as a,At as b,x as c,_ as d,B as e,Ce as f,U as g,Rt as h,Zt as i,on as j,cn as k,gn as l,wn as m,bn as n};
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{readFile as p,writeFile as I,mkdir as f}from"node:fs/promises";import{homedir as g}from"node:os";import{dirname as w,join as m}from"node:path";import{createInterface as R}from"node:readline";var a="@visiq/openclaw-plugin",c=m(g(),".openclaw","openclaw.json");function o(e,n){return new Promise(r=>e.question(n,t=>r(t)))}function v(e=process.env){return e.VISIQ_CONFIGURE_NONINTERACTIVE&&e.VISIQ_CONFIGURE_NONINTERACTIVE!=="0"?!0:!process.stdin.isTTY}function y(e=process.env){let n=(e.VISIQ_API_KEY??"").trim(),r=(e.VISIQ_AGENT_ID??"").trim(),t=(e.VISIQ_BASE_URL??"").trim();if(!n)throw new Error("VISIQ_API_KEY is required in non-interactive mode");if(!r)throw new Error("VISIQ_AGENT_ID is required in non-interactive mode");let s={apiKey:n,agentId:r};return t.length>0&&(s.baseUrl=t),s}async function E(e,n=c){let r;try{let l=await p(n,"utf-8"),i=JSON.parse(l);r=i&&typeof i=="object"&&!Array.isArray(i)?i:{}}catch{r={}}let t=r.plugins??{},s=t.entries??{},u=s[a]??{},d={apiKey:e.apiKey,agentId:e.agentId};e.baseUrl&&(d.baseUrl=e.baseUrl),s[a]={...u,config:d},t.entries=s,r.plugins=t,await f(w(n),{recursive:!0,mode:448}),await I(n,`${JSON.stringify(r,null,2)}
3
+ `,{mode:384})}async function N(){let e=R({input:process.stdin,output:process.stdout});try{process.stdout.write(`Configuring ${a}
4
+ `),process.stdout.write(`Writing to: ${c}
5
+
6
+ `);let n=(await o(e,"VisIQ API key: ")).trim();if(!n)return process.stderr.write(`No API key provided. Aborting.
7
+ `),null;let r=(await o(e,"VisIQ agent ID: ")).trim();if(!r)return process.stderr.write(`No agent ID provided. Aborting.
8
+ `),null;let t=(await o(e,"VisIQ base URL (optional, press Enter for default): ")).trim(),s={apiKey:n,agentId:r};return t.length>0&&(s.baseUrl=t),s}finally{e.close()}}async function C(){let e;if(v()?e=y():e=await N(),!e){process.exitCode=1;return}await E(e),process.stdout.write(`
9
+ Wrote credentials to ${c}
10
+ `),process.stdout.write(`You can now start OpenClaw with the VisIQ plugin enabled.
11
+ `)}var _=(()=>{let e=process.argv[1];return e?import.meta.url.endsWith(e)||import.meta.url.includes(e):!1})();_&&C().catch(e=>{process.stderr.write(`[VisIQ configure] ${e instanceof Error?e.message:String(e)}
12
+ `),process.exit(1)});export{v as isNonInteractive,y as resolveCredsFromEnv,E as writeCreds};
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Credential resolution for the VisIQ OpenClaw plugin.
3
+ *
4
+ * Resolution precedence (highest wins):
5
+ * 0. CLI flags: --api-key / --agent-id / --base-url
6
+ * 1. Environment variables: VISIQ_API_KEY / VISIQ_AGENT_ID / VISIQ_BASE_URL
7
+ * 2. JSON config file at VISIQ_CONFIG_PATH
8
+ * 3. ~/.openclaw/openclaw.json plugin config
9
+ * 4. Interactive TTY prompt (only when stdout is a TTY)
10
+ *
11
+ * Behavior on missing credentials:
12
+ * - Non-TTY context -> log a structured warning to stderr and return null
13
+ * - TTY context -> print a single-paragraph hint with the three setup
14
+ * options and return null
15
+ *
16
+ * This function NEVER throws. The plugin must remain installable / loadable
17
+ * even when unconfigured; downstream hooks fail-open (no-op) when this
18
+ * function returns null so an unconfigured plugin does not break OpenClaw.
19
+ *
20
+ * G004: never logs the resolved apiKey value, only its presence/absence.
21
+ */
22
+ export interface VisiqConfig {
23
+ apiKey: string;
24
+ agentId: string;
25
+ baseUrl?: string;
26
+ }
27
+ /**
28
+ * Resolve plugin credentials following the documented precedence chain.
29
+ *
30
+ * Resolution precedence (highest wins):
31
+ * 0. CLI flags: --api-key / --agent-id / --base-url
32
+ * 1. Environment variables: VISIQ_API_KEY / VISIQ_AGENT_ID / VISIQ_BASE_URL
33
+ * 2. JSON config file at VISIQ_CONFIG_PATH
34
+ * 3. ~/.openclaw/openclaw.json plugin config
35
+ * 4. Interactive TTY prompt (only when stdout is a TTY)
36
+ *
37
+ * Returns `null` when credentials cannot be resolved. Downstream hooks
38
+ * MUST treat null as "plugin unconfigured, no-op" and MUST NOT crash.
39
+ */
40
+ export declare function loadConfig(): Promise<VisiqConfig | null>;
41
+ /**
42
+ * @visiq/redact — shared redaction primitives for VisIQ SDKs.
43
+ *
44
+ * This package extracts the redaction logic previously embedded in
45
+ * `@visiq/sdk` so that OpenClaw plugins, LangChain wrappers, and other
46
+ * harnesses can apply consistent redaction semantics.
47
+ *
48
+ * Two operation modes:
49
+ *
50
+ * 1. **Field redaction** — for structured (object / array) content. Any
51
+ * object key listed in `spec.fields` is replaced with `spec.replacement`
52
+ * (default `"[REDACTED]"`).
53
+ *
54
+ * 2. **Pattern redaction** — for string content (or string values nested
55
+ * inside structured content). Every regex pattern in `spec.patterns` is
56
+ * compiled with the `g` flag and matched substrings are replaced with
57
+ * `spec.replacement`.
58
+ *
59
+ * Both modes can be combined in a single spec; structured content has fields
60
+ * masked first, then string leaves get pattern redaction applied.
61
+ *
62
+ * Semantics intentionally match the previous `filterDocuments`-era behavior
63
+ * in @visiq/sdk: the input is deep-cloned, so the original object is never
64
+ * mutated. Null and undefined pass through unchanged.
65
+ */
66
+ /**
67
+ * Redaction specification produced by RECALL evaluation. A non-null spec
68
+ * indicates the content must be masked before being persisted or returned to
69
+ * an agent.
70
+ */
71
+ export interface RedactionSpec {
72
+ /**
73
+ * Object keys whose values must be masked. Applied recursively at every
74
+ * depth in structured content. Empty / omitted means no field masking.
75
+ */
76
+ fields?: string[];
77
+ /**
78
+ * Regular-expression source strings (without flags). Each pattern is
79
+ * compiled with the global flag and applied to every string leaf — both
80
+ * top-level strings and string values within structured content. Empty /
81
+ * omitted means no pattern masking.
82
+ */
83
+ patterns?: string[];
84
+ /**
85
+ * Replacement string for any matched field value or matched pattern.
86
+ * Defaults to `"[REDACTED]"` when omitted.
87
+ */
88
+ replacement?: string;
89
+ }
90
+ export declare class RedactionLedger {
91
+ private readonly entries;
92
+ /** Stash a spec for a tool call. Overwrites any existing entry. */
93
+ set(toolCallId: string, spec: RedactionSpec): void;
94
+ /** Return the spec for a tool call, or undefined if none is queued. */
95
+ get(toolCallId: string): RedactionSpec | undefined;
96
+ /** Remove the spec for a tool call (idempotent). */
97
+ delete(toolCallId: string): void;
98
+ /** Drop every queued spec. Called on session_end. */
99
+ clear(): void;
100
+ /** Number of currently-queued specs. Used by before_message_write. */
101
+ size(): number;
102
+ }
103
+ /**
104
+ * Lazily create (or fetch) the ledger for `sessionId`. Callers should treat
105
+ * the returned ledger as session-scoped state that lives until session_end.
106
+ */
107
+ export declare function getLedger(sessionId: string): RedactionLedger;
108
+ /**
109
+ * Drop all redaction state for a session. Called from session_end so a
110
+ * long-running OpenClaw daemon does not accumulate ledgers indefinitely.
111
+ */
112
+ export declare function clearLedger(sessionId: string): void;
113
+ /**
114
+ * Classifier for "retrieval" tools — the tool names where ALLOW + RECALL
115
+ * must run before the tool executes.
116
+ *
117
+ * OpenClaw exposes some tools under bare names (`web_fetch`) and some under
118
+ * namespaced names (`mcp__brave__web_search`, `serverName__toolName`). For
119
+ * classification we look at the trailing segment after the last `__` and
120
+ * compare against a set of bare names so both forms route consistently.
121
+ */
122
+ /**
123
+ * Return the bare tool name (the segment after the last `__`). Tools without
124
+ * a `__` separator are returned unchanged.
125
+ */
126
+ export declare function bareToolName(toolName: string): string;
127
+ /**
128
+ * True if the tool participates in retrieval semantics and therefore must be
129
+ * routed through ALLOW + RECALL evaluation in `before_tool_call`.
130
+ *
131
+ * Non-retrieval tools bypass the evaluation entirely so the plugin does not
132
+ * add latency to write tools (whose authorization is handled elsewhere).
133
+ */
134
+ export declare function isRetrievalTool(toolName: string): boolean;
135
+ /**
136
+ * Inspection helper for tests / debugging — returns the configured set of
137
+ * bare retrieval tool names.
138
+ */
139
+ export declare function getRetrievalToolNames(): ReadonlySet<string>;
140
+ /**
141
+ * True when the tool performs a governed side effect and must be gated by ALLOW.
142
+ *
143
+ * Decision rule (fail-closed, G001):
144
+ * - Retrieval tools are NEVER actions (they are gated by RECALL instead).
145
+ * - OpenClaw framework-internal tools are NEVER actions (host plumbing).
146
+ * - Everything else IS an action — both the explicitly-listed write tools and
147
+ * any unknown/unclassified agent tool. Gating unknown writes by default is
148
+ * the safe behavior.
149
+ */
150
+ export declare function isActionTool(toolName: string): boolean;
151
+ /**
152
+ * True when the tool is in the EXPLICIT, well-known action set. Distinct from
153
+ * `isActionTool`, which also returns true for unknown tools. Use this when you
154
+ * need to know whether a tool was specifically recognized (e.g. diagnostics),
155
+ * not merely defaulted into the action bucket.
156
+ */
157
+ export declare function isKnownActionTool(toolName: string): boolean;
158
+ /**
159
+ * Inspection helper for tests / debugging — returns the configured set of
160
+ * explicit bare action tool names.
161
+ */
162
+ export declare function getActionToolNames(): ReadonlySet<string>;
163
+ export interface OpenclawHook {
164
+ pluginId: string;
165
+ hookName: string;
166
+ handler: (event: unknown, ctx: unknown) => unknown;
167
+ priority?: number;
168
+ source: string;
169
+ }
170
+ export interface OpenclawPlugin {
171
+ id: string;
172
+ name: string;
173
+ version: string;
174
+ hooks: OpenclawHook[];
175
+ }
176
+ /**
177
+ * Build the plugin descriptor consumed by OpenClaw's plugin loader.
178
+ */
179
+ export declare function createPlugin(): OpenclawPlugin;
180
+
181
+ export {
182
+ createPlugin as default,
183
+ };
184
+
185
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{a as g,c as w,d as _,e as d,f as e,g as r,h as k,i,j as s,k as l,l as a,m as u,n as p}from"./chunk-K2XSEUBX.js";var c=new Set(["place_hold","block_card","wire_transfer","create_case","send_notification","bank_account_lookup_tool","exec","write_file","http_post","http_put","http_delete"]),f=new Set(["sessions_list","sessions_history","sessions_send","sessions_get","sessions_create","sessions_delete","memory_search","memory_read","memory_write"]),h=["sessions_","memory_"];function N(o){let t=e(o);return f.has(t)?!0:h.some(m=>t.startsWith(m))}function T(o){return!(r(o)||N(o))}function I(o){return c.has(e(o))}function x(){return c}var n="@visiq/openclaw-plugin";function O(){return{id:n,name:"VisIQ OpenClaw Plugin",version:"0.1.0",hooks:[{pluginId:n,hookName:"before_tool_call",handler:i,source:"visiq-openclaw-plugin"},{pluginId:n,hookName:"tool_result_persist",handler:s,source:"visiq-openclaw-plugin"},{pluginId:n,hookName:"before_message_write",handler:l,source:"visiq-openclaw-plugin"},{pluginId:n,hookName:"llm_input",handler:a,source:"visiq-openclaw-plugin"},{pluginId:n,hookName:"llm_output",handler:u,source:"visiq-openclaw-plugin"},{pluginId:n,hookName:"session_end",handler:p,source:"visiq-openclaw-plugin"}]}}var C=O;export{w as RedactionLedger,e as bareToolName,d as clearLedger,O as createPlugin,C as default,x as getActionToolNames,_ as getLedger,k as getRetrievalToolNames,T as isActionTool,I as isKnownActionTool,r as isRetrievalTool,g as loadConfig};
@@ -0,0 +1,5 @@
1
+ import{b as w,i as b,j as S,k,l as L,m as v,n as O}from"./chunk-K2XSEUBX.js";import{Type as e}from"typebox";import{definePluginEntry as D}from"openclaw/plugin-sdk/plugin-entry";import{readFileSync as x,readdirSync as A,statSync as C,existsSync as R}from"node:fs";import{join as p,dirname as $}from"node:path";import{fileURLToPath as P}from"node:url";var U="@visiq/openclaw-plugin",h=process.env.BANK_API_URL??"http://localhost:4000/api",I=$(P(import.meta.url));function c(n,i={}){return{content:[{type:"text",text:n}],details:i}}async function m(n){let i=await fetch(`${h}${n}`);return i.ok?await i.text():`[Error ${i.status}]`}async function d(n,i){let t=await fetch(`${h}${n}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return t.ok?await t.text():`[Error ${t.status}]`}function j(){let n=[p(I,"..","..","..","examples","langchain-agent","docs"),p(I,"..","..","examples","langchain-agent","docs"),process.env.VISIQ_KNOWLEDGE_DIR??""].filter(i=>i.length>0);for(let i of n)if(R(i))return i;return null}function N(){let n=j();if(!n)return[];let i=[];function t(o){for(let s of A(o)){let r=p(o,s);if(C(r).isDirectory())t(r);else if(s.endsWith(".txt")){let l=x(r,"utf-8").match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);if(l){let a={};for(let _ of l[1].split(`
2
+ `)){let[g,...y]=_.split(":");g&&y.length&&(a[g.trim()]=y.join(":").trim())}i.push({path:r,title:a.title?.replace(/"/g,"")??s,classification:a.classification??"public",source:a.source??"unknown",content:l[2].trim()})}}}}return t(n),i}var f=null;function B(){return f===null&&(f=N()),f}function T(n,i=5){let t=B(),o=n.toLowerCase().split(/\s+/).filter(r=>r.length>=3);return t.map(r=>{let u=`${r.title} ${r.content}`.toLowerCase(),l=0;for(let a of o)u.includes(a)&&(l+=a.length,r.title.toLowerCase().includes(a)&&(l+=a.length*2));return{doc:r,score:l}}).filter(r=>r.score>0).sort((r,u)=>u.score-r.score).slice(0,i).map(r=>r.doc)}function K(n){w(),n.on("before_tool_call",b,{priority:50}),n.on("tool_result_persist",S),n.on("before_message_write",k),n.on("llm_input",L),n.on("llm_output",v),n.on("session_end",O),n.registerTool({name:"get_account",label:"Get account",description:"Get account holder profile from core banking system. Input: account ID (e.g. '4821-7734'). Returns: holder name, balance, risk score, KYC status, linked cards, address.",parameters:e.Object({account_id:e.String({description:"Account ID (e.g. '4821-7734')."})}),async execute(i,t){return c(await m(`/accounts/${t.account_id}`))}}),n.registerTool({name:"get_devices",label:"Get devices",description:"Get device fingerprints and login sessions from BioCatch. Input: account ID. Returns: registered devices, recent sessions with IP/geolocation, anomaly alerts.",parameters:e.Object({account_id:e.String({description:"Account ID to fetch devices for."})}),async execute(i,t){return c(await m(`/accounts/${t.account_id}/devices`))}}),n.registerTool({name:"get_transactions",label:"Get transactions",description:"Get recent transactions from the card processor with filtering. Returns: transaction list with amounts, merchants, locations, and risk flags.",parameters:e.Object({account_id:e.String({description:"Account ID (e.g. '4821-7734')."}),lookback_hours:e.Optional(e.Integer({minimum:1,maximum:720,description:"How far back to look (1\u2013720h, default 72)."})),min_amount:e.Optional(e.Number({minimum:0,description:"Filter out transactions below this dollar amount."}))}),async execute(i,t){let o=new URLSearchParams;t.lookback_hours!==void 0&&o.set("lookback_hours",String(t.lookback_hours)),t.min_amount!==void 0&&o.set("min_amount",String(t.min_amount));let s=o.toString()?`?${o.toString()}`:"";return c(await m(`/accounts/${t.account_id}/transactions${s}`))}}),n.registerTool({name:"check_watchlist",label:"Check watchlist",description:"Screen a name or entity against OFAC SDN, EU, and UN sanctions lists. Returns: match scores and recommendation.",parameters:e.Object({name:e.String({description:"Name or entity to screen."}),country_code:e.Optional(e.String({description:"ISO 3166-1 alpha-2 country code to scope the screening (e.g. 'US')."}))}),async execute(i,t){return c(await d("/watchlists/check",{name:t.name,country_code:t.country_code}))}}),n.registerTool({name:"search_knowledge",label:"Search knowledge base",description:"Search the bank's internal knowledge base for compliance policies, prior fraud case files, triage procedures, and customer information. Input is a free-text search query.",parameters:e.Object({query:e.String({description:"Free-text knowledge base search query."})}),async execute(i,t){let o=T(t.query,5);if(o.length===0)return c("No matching knowledge base documents found.",{hitCount:0});let s=o.map(r=>`--- ${r.title} (classification=${r.classification}, source=${r.source}) ---
3
+ ${r.content}`).join(`
4
+
5
+ `);return c(s,{hitCount:o.length,classifications:o.map(r=>r.classification)})}}),n.registerTool({name:"place_hold",label:"Place hold",description:"Place a temporary fraud hold on an account. Blocks outbound transactions while allowing inbound credits.",parameters:e.Object({account_id:e.String({description:"Account to place the hold on."}),amount:e.Number({minimum:0,description:"Dollar amount to hold (0 = full account freeze)."}),currency:e.Union([e.Literal("USD"),e.Literal("EUR"),e.Literal("GBP"),e.Literal("CAD")],{description:"ISO currency code for the hold amount."}),duration_hours:e.Integer({minimum:1,maximum:168,description:"How long the hold stays (1\u2013168h)."}),reason:e.Union([e.Literal("fraud_investigation"),e.Literal("suspicious_pattern"),e.Literal("sanctions_review"),e.Literal("manual_review")],{description:"Reason classification for the hold."}),notify_holder:e.String({description:"Whether to notify the holder. Accepts: yes, no, true, false, 1, 0."})}),async execute(i,t){let{account_id:o,...s}=t;return c(await d(`/accounts/${o}/hold`,s))}}),n.registerTool({name:"block_card",label:"Block card",description:"Block a debit or credit card immediately. A replacement card will be mailed.",parameters:e.Object({account_id:e.String({description:"Account ID owning the card."}),reason:e.Union([e.Literal("fraud_investigation"),e.Literal("lost"),e.Literal("stolen"),e.Literal("compromised_pin"),e.Literal("customer_request")],{description:"Reason for blocking \u2014 drives the customer notification."}),notify_holder:e.Optional(e.String({description:"Whether to notify the holder. Accepts: yes, no, true, false."}))}),async execute(i,t){return c(await d(`/accounts/${t.account_id}/card/block`,{reason:t.reason,notify_holder:t.notify_holder}))}}),n.registerTool({name:"wire_transfer",label:"Wire transfer",description:"Initiate an outbound wire transfer from an account. HIGH RISK \u2014 large or sanctioned-destination wires are blocked or escalated by VisIQ ALLOW policy.",parameters:e.Object({account_id:e.String({description:"Source account ID."}),amount:e.Number({minimum:0,description:"Dollar amount to wire."}),currency:e.Union([e.Literal("USD"),e.Literal("EUR"),e.Literal("GBP"),e.Literal("CAD")],{description:"ISO currency code."}),destination:e.String({description:"Destination account / IBAN / beneficiary identifier."}),destination_country:e.Optional(e.String({description:"ISO 3166-1 alpha-2 destination country (e.g. 'US')."})),memo:e.Optional(e.String({description:"Free-text wire memo."}))}),async execute(i,t){let{account_id:o,...s}=t;return c(await d(`/accounts/${o}/wire`,s))}}),n.registerTool({name:"create_case",label:"Create case",description:"Create a fraud investigation case. Routes to the right queue based on severity / category.",parameters:e.Object({account_id:e.String({description:"Affected account ID."}),severity:e.Union([e.Literal("low"),e.Literal("medium"),e.Literal("high"),e.Literal("critical")],{description:"Case severity \u2014 drives SLA and routing."}),category:e.Union([e.Literal("account_takeover"),e.Literal("card_fraud"),e.Literal("wire_fraud"),e.Literal("synthetic_identity"),e.Literal("sanctions_hit"),e.Literal("structuring")],{description:"Fraud category \u2014 routes to specialist queue."}),summary:e.String({description:"Free-text case summary for the analyst."}),assignee:e.Optional(e.String({description:"Specialist email to assign directly."})),sla_hours:e.Optional(e.Integer({minimum:1,maximum:168,description:"Override the default SLA (1\u2013168h)."}))}),async execute(i,t){return c(await d("/cases",t))}}),n.registerTool({name:"send_notification",label:"Send notification",description:"Send a fraud alert notification to the customer. Channel + priority drive delivery latency.",parameters:e.Object({account_id:e.String({description:"Account ID \u2014 used to look up contact methods."}),channel:e.Union([e.Literal("sms"),e.Literal("email"),e.Literal("push"),e.Literal("voice"),e.Literal("in_app")],{description:"Delivery channel."}),priority:e.Union([e.Literal("low"),e.Literal("normal"),e.Literal("high"),e.Literal("critical")],{description:"Delivery priority \u2014 critical bypasses customer quiet hours."}),subject:e.String({maxLength:120,description:"Short subject line (used for email / push title)."}),message:e.String({description:"Body text of the notification."}),locale:e.Optional(e.String({description:"BCP-47 locale (e.g. 'en-US', 'es-MX')."}))}),async execute(i,t){return c(await d("/notifications/send",t))}}),n.registerTool({name:"bank_account_lookup_tool",label:"Bank account lookup (sensitive)",description:"Look up sensitive account data for fraud investigation. account_id is the account number (e.g. '4821-7734'). include_ssn controls whether PII fields (SSN, DOB, full legal name) come back \u2014 accepts 'yes' / 'no' / 'true' / 'false' / '1' / '0'.",parameters:e.Object({account_id:e.String({description:"Account number to look up (e.g. 4821-7734)."}),include_ssn:e.String({description:"Whether to include PII fields (SSN, DOB, full legal name). Accepts: yes, no, true, false, 1, 0."})}),async execute(i,t){let s=new Set(["yes","y","true","1"]).has(String(t.include_ssn).toLowerCase().trim()),r=await fetch(`${h}/accounts/${t.account_id}`);if(!r.ok)return c(JSON.stringify({error:`Account ${t.account_id} not found`}),{status:r.status});let u=await r.json();if(!s){let{ssn:l,dob:a,full_legal_name:_,...g}=u;return c(JSON.stringify({account:g,pii_included:!1}),{pii_included:!1})}return c(JSON.stringify({account:u,pii_included:!0}),{pii_included:!0})}})}var Y=D({id:U,name:"VisIQ OpenClaw Plugin",description:"Routes tool calls and LLM I/O through VisIQ ALLOW + RECALL + RECORD",register:K});export{Y as default};
@@ -0,0 +1,47 @@
1
+ {
2
+ "id": "@visiq/openclaw-plugin",
3
+ "name": "VisIQ OpenClaw Plugin",
4
+ "version": "0.1.4",
5
+ "description": "Routes tool calls and LLM I/O through VisIQ ALLOW+RECALL+RECORD",
6
+ "main": "dist/index.js",
7
+ "activation": {
8
+ "onStartup": true
9
+ },
10
+ "contracts": {
11
+ "tools": [
12
+ "get_account",
13
+ "get_devices",
14
+ "get_transactions",
15
+ "check_watchlist",
16
+ "search_knowledge",
17
+ "place_hold",
18
+ "block_card",
19
+ "wire_transfer",
20
+ "create_case",
21
+ "send_notification",
22
+ "bank_account_lookup_tool"
23
+ ]
24
+ },
25
+ "skills": [
26
+ "skills/fraud-triage",
27
+ "skills/investigate"
28
+ ],
29
+ "configSchema": {
30
+ "type": "object",
31
+ "properties": {
32
+ "apiKey": {
33
+ "type": "string",
34
+ "description": "VisIQ API key (also: VISIQ_API_KEY env var)"
35
+ },
36
+ "agentId": {
37
+ "type": "string",
38
+ "description": "VisIQ agent ID (also: VISIQ_AGENT_ID env var)"
39
+ },
40
+ "baseUrl": {
41
+ "type": "string",
42
+ "description": "VisIQ API base URL (optional; also: VISIQ_BASE_URL / VISIQ_ENDPOINT env var)"
43
+ }
44
+ },
45
+ "required": []
46
+ }
47
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@visiq/openclaw-plugin",
3
+ "version": "0.1.4",
4
+ "description": "OpenClaw plugin for VisIQ ALLOW + RECALL + RECORD",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "openclaw": {
9
+ "extensions": [
10
+ "./dist/openclaw-entry.js"
11
+ ],
12
+ "compat": {
13
+ "pluginApi": ">=2026.5.18",
14
+ "minGatewayVersion": "2026.5.18"
15
+ },
16
+ "build": {
17
+ "openclawVersion": "2026.5.18",
18
+ "pluginSdkVersion": "2026.5.18"
19
+ }
20
+ },
21
+ "bin": {
22
+ "visiq-openclaw-plugin": "./dist/cli/configure.js"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@open-policy-agent/opa-wasm": "^1.9.0",
32
+ "zod": "^3.23.8",
33
+ "typebox": "^1.1.38"
34
+ },
35
+ "peerDependencies": {
36
+ "@langchain/core": ">=0.3.0",
37
+ "llamaindex": ">=0.11.0",
38
+ "openclaw": ">=2026.5.18"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@langchain/core": {
42
+ "optional": true
43
+ },
44
+ "llamaindex": {
45
+ "optional": true
46
+ },
47
+ "openclaw": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "license": "MIT",
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "openclaw.plugin.json",
59
+ "skills"
60
+ ],
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/VISIQ-LABS/xy.git",
67
+ "directory": "packages/visiq-openclaw-plugin"
68
+ },
69
+ "keywords": [
70
+ "visiq",
71
+ "openclaw",
72
+ "plugin",
73
+ "ai-governance",
74
+ "allow",
75
+ "recall",
76
+ "record"
77
+ ]
78
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: fraud-triage
3
+ description: Investigate a bank fraud alert by calling the VisIQ bank tools and take protective action. Every tool call is governed by VisIQ ALLOW (actions) + RECALL (retrievals) + RECORD.
4
+ user-invocable: true
5
+ metadata: { "openclaw": { "emoji": "🛡️" } }
6
+ ---
7
+
8
+ # Fraud Triage
9
+
10
+ You are an AI fraud triage analyst at a major US retail bank. You investigate
11
+ fraud alerts EXCLUSIVELY by calling tools — you cannot reason about an account
12
+ without first fetching its data.
13
+
14
+ The VisIQ governance plugin owns these tools. RETRIEVAL tools are gated by
15
+ RECALL (read authorization + PII redaction); ACTION tools are gated by ALLOW
16
+ (allow / deny / human-in-the-loop escalation). Every model turn is RECORDed.
17
+
18
+ ## Available tools
19
+
20
+ Retrieval (read): `get_account`, `get_transactions`, `get_devices`,
21
+ `check_watchlist`, `search_knowledge`
22
+
23
+ Actions (write): `place_hold`, `block_card`, `wire_transfer`, `create_case`,
24
+ `send_notification`, `bank_account_lookup_tool`
25
+
26
+ ## Workflow (mandatory)
27
+
28
+ 1. FIRST turn (in parallel): call `get_account`, `get_transactions`,
29
+ `get_devices`, and `search_knowledge` for the alerted account.
30
+ 2. After tool results return, identify anomalies (impossible travel,
31
+ unrecognized devices, sanctions hits via `check_watchlist`).
32
+ 3. If the alert suggests identity theft or you need PII to file a case, call
33
+ `bank_account_lookup_tool` — pass `include_ssn="yes"` only when PII is
34
+ genuinely required (identity verification, regulatory filings); otherwise
35
+ pass `include_ssn="no"` to keep the response sensitive-data-free.
36
+ 4. Take protective action via tools: `place_hold`, `block_card`, `create_case`,
37
+ `send_notification`. Use `wire_transfer` only to claw back or reroute funds
38
+ when policy permits — large or sanctioned-destination wires will be blocked
39
+ or escalated by ALLOW.
40
+ 5. Only after every relevant tool has been called, write a final summary.
41
+
42
+ ## Hard rules
43
+
44
+ - DO NOT respond with prose plans or narrative summaries before calling tools —
45
+ the workflow above is already approved. Just call the tools.
46
+ - You may place holds up to $25,000; escalate larger amounts via `create_case`.
47
+ - You may NOT file SARs — escalate to compliance via `create_case`.
48
+ - Block cards for confirmed unauthorized activity. Always `send_notification`
49
+ after a protective action.
50
+ - `bank_account_lookup_tool`'s `include_ssn` argument accepts the natural
51
+ variants "yes" / "no" / "true" / "false" / "1" / "0" — pick whichever reads
52
+ cleanly in context; the platform normalizes them.
53
+
54
+ Call the tools. Do not narrate; act.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: investigate
3
+ description: One-command bank fraud investigation of the flagged account (4821-7734). Fetches the account, transactions, devices, knowledge base and sanctions watchlist, then takes protective action — every tool call governed by VisIQ ALLOW (actions) + RECALL (retrievals) + RECORD.
4
+ user-invocable: true
5
+ metadata: { "openclaw": { "emoji": "🔎" } }
6
+ ---
7
+
8
+ # Investigate
9
+
10
+ You are an AI fraud-triage analyst at a major US retail bank. Investigate the
11
+ alert below EXCLUSIVELY by calling the VisIQ bank tools — you cannot reason about
12
+ the account without first fetching its data. Begin immediately; do not ask the
13
+ user for more detail.
14
+
15
+ ## The alert — investigate this now
16
+
17
+ **Account 4821-7734.** Three ATM withdrawals of **$3,000 each** ($9,000 total)
18
+ from **Lagos, Nigeria** in the last hour. The account holder lives in **Chicago**
19
+ with no travel notice on file — a classic impossible-travel pattern.
20
+
21
+ Pull the account, its transactions and its devices; search the knowledge base for
22
+ the customer file; screen the holder against the sanctions watchlist; then take
23
+ protective action — place a fraud hold and block the card.
24
+
25
+ ## Available tools
26
+
27
+ Retrieval (read): `get_account`, `get_transactions`, `get_devices`,
28
+ `check_watchlist`, `search_knowledge`
29
+
30
+ Actions (write): `place_hold`, `block_card`, `wire_transfer`, `create_case`,
31
+ `send_notification`, `bank_account_lookup_tool`
32
+
33
+ The VisIQ governance plugin owns these tools. RETRIEVAL tools are gated by RECALL
34
+ (read authorization + PII redaction); ACTION tools are gated by ALLOW (allow /
35
+ deny / human-in-the-loop escalation). Every model turn is RECORDed.
36
+
37
+ ## Workflow (mandatory)
38
+
39
+ 1. FIRST turn (in parallel): call `get_account`, `get_transactions`,
40
+ `get_devices`, and `search_knowledge` for account 4821-7734.
41
+ 2. After tool results return, identify anomalies (impossible travel,
42
+ unrecognized devices, sanctions hits via `check_watchlist`).
43
+ 3. If you need PII to file a case, call `bank_account_lookup_tool` — pass
44
+ `include_ssn="yes"` only when PII is genuinely required (identity
45
+ verification, regulatory filings); otherwise pass `include_ssn="no"`.
46
+ 4. Take protective action via tools: `place_hold`, `block_card`, `create_case`,
47
+ `send_notification`. Use `wire_transfer` only to claw back or reroute funds
48
+ when policy permits — large or sanctioned-destination wires will be blocked or
49
+ escalated by ALLOW.
50
+ 5. Only after every relevant tool has been called, write a final summary.
51
+
52
+ ## Hard rules
53
+
54
+ - DO NOT respond with prose plans or narrative summaries before calling tools —
55
+ the workflow above is already approved. Just call the tools.
56
+ - You may place holds up to $25,000; escalate larger amounts via `create_case`.
57
+ - You may NOT file SARs — escalate to compliance via `create_case`.
58
+ - Block cards for confirmed unauthorized activity. Always `send_notification`
59
+ after a protective action.
60
+ - `bank_account_lookup_tool`'s `include_ssn` argument accepts "yes" / "no" /
61
+ "true" / "false" / "1" / "0" — the platform normalizes them.
62
+
63
+ Call the tools. Do not narrate; act.