@lark-tns/openclaw-guardian-plugin 2026.4.15 → 2026.4.16
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/dist/index.js +8 -0
- package/package.json +12 -9
- package/src/common-payload.ts +0 -27
- package/src/detect-service.ts +0 -16
- package/src/hook-handler/index.ts +0 -269
- package/src/hook-payloads/index.ts +0 -45
- package/src/hook-payloads/llm-fetch-payloads.ts +0 -26
- package/src/hook-payloads/llm-payloads.ts +0 -33
- package/src/hook-payloads/shared.ts +0 -78
- package/src/hook-payloads/tool-payloads.ts +0 -33
- package/src/hostname.ts +0 -10
- package/src/http-client.ts +0 -233
- package/src/index.ts +0 -31
- package/src/llm-fetch/index.ts +0 -485
- package/src/llm-fetch/request-utils.ts +0 -94
- package/src/llm-fetch/response-utils.ts +0 -70
- package/src/llm-fetch/types.ts +0 -34
- package/src/logger.ts +0 -84
- package/src/plugin-env.ts +0 -14
- package/src/runtime-config.ts +0 -252
- package/src/skill-scan/hook-handler.ts +0 -119
- package/src/skill-scan/index.ts +0 -173
- package/src/skill-scan/skill-detect-client.ts +0 -178
- package/src/skill-scan/skill-scanner.ts +0 -331
- package/src/skill-scan/skill-storage.ts +0 -185
- package/src/token-auth.ts +0 -227
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{definePluginEntry as t}from"openclaw/plugin-sdk/plugin-entry";import{basename as n,dirname as r,join as i,relative as a,resolve as o}from"node:path";import{homedir as s,hostname as c}from"node:os";import{lstat as l,mkdir as u,readFile as d,readdir as f,rename as p,stat as m,unlink as h,writeFile as g}from"node:fs/promises";import{createHash as _}from"node:crypto";import{existsSync as v}from"node:fs";var y=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),b=e(import.meta.url);const x=2e4;function S(e){return`[openclaw-guardian-plugin] ${e}`}function C(){let e=new WeakSet;return(t,n)=>{if(n instanceof Error)return{name:n.name,message:n.message,stack:n.stack};if(typeof n==`bigint`)return n.toString();if(typeof n==`object`&&n){if(e.has(n))return`[Circular]`;e.add(n)}return n}}function w(e){let t=JSON.stringify(e,C(),2)??`null`;return t.length<=x?t:`${t.slice(0,x)}\n...<truncated>`}function T(e,t){return t===`online`?`[payload omitted in online]`:w(e)}function E(e,t,n,r){e.logger.info(S(`[${t}] ${T(n,r)}`))}function ee(e){return typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`?String(e):JSON.stringify(e,C(),2)??`null`}function D(e){let t=e?.pluginConfig?.env;return t===`boe`||t===`dev`||t===`pre`?t:`online`}let O=null,k=null;function A(e,t){if(typeof e!=`string`||!e)throw Error(`missing or invalid ${t}`);return e}function j(...e){for(let t of e)if(typeof t==`string`&&t)return t;return null}function M(e){return!e||typeof e!=`object`||Array.isArray(e)?null:e}function N(e){let t=j(e?.pluginConfig?.appId),n=j(e?.pluginConfig?.appSecret),r=t&&n?{appId:t,appSecret:n}:null,i=M(M(M(e?.config)?.channels)?.feishu),a=j(i?.appId,i?.app_id),o=j(i?.appSecret,i?.app_secret),s=a&&o?{appId:a,appSecret:o}:null,c=D(e);return c===`online`||c===`pre`?s??r??null:r??s??null}function P(e){return!!N(e)}function F(e){let t=e?.pluginConfig?.x_tt_env;return typeof t==`string`&&t?t:D(e)===`boe`?`boe_tns_api`:null}function te(e){let t=D(e);return t===`online`?`https://open.feishu.cn`:t===`pre`?`https://open.feishu-pre.cn`:`https://open.feishu-boe.cn`}function ne(e){if(!e||typeof e!=`object`||Array.isArray(e))return null;let t=e;return{code:typeof t.code==`number`?t.code:NaN,msg:typeof t.msg==`string`?t.msg:``,tenant_access_token:typeof t.tenant_access_token==`string`?t.tenant_access_token:void 0,expire:typeof t.expire==`number`?t.expire:void 0}}async function I(e){let t=Date.now();return O&&O.expiresAtMs-t>18e5?O.token:k||(k=(async()=>{let t=N(e);if(!t)throw Error(`missing Feishu appId/appSecret from api.config.channels.feishu or pluginConfig`);let n=A(t.appId,`appId`),r=A(t.appSecret,`appSecret`),i=`${te(e)}/open-apis/auth/v3/tenant_access_token/internal`,a={"content-type":`application/json`},o=F(e);o&&(a[`x-tt-env`]=o),e.logger.debug?.(S(`[auth] requesting tenant_access_token`));let{signal:s,clear:c}=ie(U),l;try{l=await fetch(i,{method:`POST`,headers:a,body:JSON.stringify({app_id:n,app_secret:r}),signal:s})}catch(e){throw c(),e instanceof Error&&e.name===`AbortError`?new z(i,U):new B(i,e)}finally{c()}let u=await l.text();if(!l.ok)throw Error(`[auth] tenant_access_token request failed status=${l.status} body=${u||`<empty>`}`);let d=null;try{d=u?JSON.parse(u):null}catch{d=null}let f=ne(d);if(!f||!Number.isFinite(f.code))throw Error(`[auth] tenant_access_token response parse failed`);if(f.code!==0)throw Error(`[auth] tenant_access_token request failed code=${f.code}${f.msg?` msg=${f.msg}`:``}`);let p=A(f.tenant_access_token,`tenant_access_token`),m=typeof f.expire==`number`&&f.expire>0?f.expire:7200;return O={token:p,expiresAtMs:Date.now()+m*1e3},e.logger.debug?.(S(`[auth] tenant_access_token updated`)),p})().finally(()=>{k=null}),k)}async function L(e){if(D(e)===`dev`)return{};let t={},n=F(e);return n&&(t[`x-tt-env`]=n),t.authorization=`Bearer ${await I(e)}`,t}async function R(e,t={}){let n=await L(e),r=new Headers(t.headers??{});for(let[e,t]of Object.entries(n))r.set(e,t);return{...t,headers:r}}var z=class extends Error{code=`TIMEOUT`;constructor(e,t){super(`Request to ${e} timed out after ${t}ms`),this.name=`RequestTimeoutError`}},B=class extends Error{code=`NETWORK`;constructor(e,t){super(`Network error requesting ${e}: ${t instanceof Error?t.message:String(t)}`),this.name=`NetworkError`}},re=class extends Error{code=`SERVER`;statusCode;constructor(e,t,n){super(`Server error ${t} from ${e}${n?`: ${n}`:``}`),this.name=`ServerError`,this.statusCode=t}};function V(e){return e instanceof z?`TIMEOUT`:e instanceof B?`NETWORK`:e instanceof re?`SERVER`:e instanceof Error&&e.name===`AbortError`?`TIMEOUT`:`UNKNOWN`}const H=3e4,U=3e4;function ie(e){let t=new AbortController,n=setTimeout(()=>t.abort(),e);return typeof n==`object`&&`unref`in n&&n.unref(),{signal:t.signal,clear:()=>clearTimeout(n)}}function ae(e,t){if(!e)return t;let n=new AbortController,r=()=>n.abort();return e.addEventListener(`abort`,r,{once:!0}),t.addEventListener(`abort`,r,{once:!0}),n.signal}const oe={dev:`http://localhost:8001`,online:`https://open.feishu.cn`,boe:`https://open.feishu-boe.cn`,pre:`https://open.feishu-pre.cn`};function W(e){return`${oe[D(e)]}/open-apis/security_plugin/v1/openclaw_plugin`}function se(e){return`${W(e)}/config`}function ce(e){return`${W(e)}/detect`}function le(e){return`${W(e)}/batch_check_skill_detection`}function ue(e){return`${W(e)}/skill_detect`}function de(e={}){let t={};for(let[n,r]of Object.entries(e??{}))t[n]=ee(r);return t}function fe(e,t,n=``){return{hook_name:e,payload:de(t),source:n}}function pe(e){if(!e)return null;try{let t=JSON.parse(e);return t&&typeof t==`object`&&!Array.isArray(t)?t:null}catch{return null}}function me(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.code==`number`&&Number.isFinite(t.code)&&typeof t.msg==`string`}async function he(e,t,n){let r=await n.text();if(e.logger.debug?.(S(`[${t}] response: ${r}`)),!n.ok)throw Error(`${t} request failed: status=${n.status} body=${r||`<empty>`}`);let i=pe(r);if(!me(i))throw Error(`${t} response parse failed: missing or invalid code/msg fields`);if(i.code!==0)throw Error(`${t} request failed: code=${i.code}${i.msg?` msg=${i.msg}`:``}`);return i.data??null}async function G(e,t,n,r,i=H){let a=await R(e,r),{signal:o,clear:s}=ie(i),c=ae(a.signal,o),l,u=Date.now();try{l=await fetch(n,{...a,signal:c});let r=Date.now()-u;e.logger.info(S(`[${t}] http_elapsed=${r}ms status=${l.status} url=${n}`))}catch(r){let a=Date.now()-u;throw e.logger.info(S(`[${t}] http_elapsed=${a}ms error=${r instanceof Error?r.message:String(r)} url=${n}`)),s(),r instanceof Error&&r.name===`AbortError`?new z(n,i):new B(n,r)}finally{s()}return he(e,t,l)}let ge=null;function _e(){return ge||=c(),ge}function ve(){return{hostname:_e()}}function ye(e){if(!e||typeof e!=`object`||Array.isArray(e))return e;let t=e;return Object.prototype.hasOwnProperty.call(t,`hostname`)?e:{...t,...ve()}}async function K(e,t,n){let r=fe(t,ye(n),`miaoda`);return G(e,t,ce(e),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r)},H)}function q(...e){return e.find(e=>e!=null)}function J(e){return e==null?``:typeof e==`string`?e:String(e)}function be(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:{}}function xe(e){return Array.isArray(e)?e:[]}function Se(e){try{let t=new URL(e);return{domain:t.host,path:t.pathname||`/`}}catch{return{domain:``,path:``}}}function Ce(e){let{domain:t,path:n}=Se(J(e?.url));return{request_id:J(e?.request_id),domain:t,path:n,origin_req:q(e?.jsonBody,e?.rawBody,``)}}function we(e){let{domain:t,path:n}=Se(J(e?.url));return{request_id:J(e?.request_id),domain:t,path:n,is_sse:e?.isSse?`true`:`false`,chunks:xe(e?.chunks),origin_resp:e?.isSse?``:J(q(e?.originResp,``))}}function Te(e,t){return{agent_name:J(t?.agentId),session_id:J(t?.sessionId),session_key:J(t?.sessionKey),channel_id:J(t?.channelId),message_provider:J(t?.messageProvider),trigger:J(t?.trigger),model:J(e?.model),provider:J(e?.provider),run_id:J(e?.runId)}}function Ee(e,t){return{...Te(e,t),prompt:J(e?.prompt),system_prompt:J(e?.systemPrompt),history_messages:xe(e?.historyMessages)}}function De(e,t){return{...Te(e,t),assistant_texts:xe(e?.assistantTexts),last_assistant:J(JSON.stringify(e?.lastAssistant))}}function Oe(e,t){return{agent_name:J(t?.agentId),session_id:J(t?.sessionId),session_key:J(t?.sessionKey),tool_call_id:J(e?.toolCallId),tool_name:J(e?.toolName),tool_args:q(e?.params,{}),run_id:J(e?.runId)}}function ke(e,t){return Oe(e,t)}function Ae(e,t){return{...Oe(e,t),tool_output:q(e?.result,``),tool_error:q(e?.error,``),tool_duration_ms:J(q(e?.durationMs,``))}}function Y(e,t,n,r){let i=t,a=t,o=n;switch(e){case`before_tool_call`:return ke(i,o);case`after_tool_call`:return Ae(i,o);case`before_llm_fetch`:return Ce(r);case`after_llm_fetch`:return we(r);case`llm_input`:return Ee(a,o);case`llm_output`:return De(a,o);default:return be(r)}}function je(e){return i(e??i(s(),`.openclaw`,`workspace`),`.openclaw-guardian`,`skill-detect-state.json`)}const Me=je();function Ne(e){if(!e||typeof e!=`object`||!(`code`in e))return;let{code:t}=e;return typeof t==`string`?t:void 0}function Pe(e,t){return e.is_detected?typeof e.last_detect_ts==`number`?(t??Date.now())-e.last_detect_ts>864e5:!0:!1}function Fe(e){return!e.dir_hash||!e.zip_hash}let Ie=Promise.resolve();function Le(e){let t=Ie.then(e,e);return Ie=t.catch(()=>void 0),t}async function X(e){try{await h(e)}catch{}}async function Re(e,t){return Le(async()=>{let n=t??Me;e.logger.info(S(`[skill-storage] loading state from: ${n}`));try{let t=await d(n,`utf-8`),r=JSON.parse(t);return!r||typeof r!=`object`||Array.isArray(r)?(e.logger.warn(S(`[skill-storage] corrupted state file (invalid format), deleting and starting fresh`)),await X(n),{}):(e.logger.info(S(`[skill-storage] loaded ${Object.keys(r).length} skill(s) from state`)),r)}catch(t){return Ne(t)===`ENOENT`?(e.logger.info(S(`[skill-storage] no existing state file, starting fresh`)),{}):t instanceof SyntaxError?(e.logger.warn(S(`[skill-storage] corrupted state file (invalid JSON), deleting and starting fresh`)),await X(n),{}):(e.logger.warn(S(`[skill-storage] failed to load state: ${t instanceof Error?t.message:String(t)}`)),{})}})}async function ze(e,t,n){return Le(async()=>{let i=n??Me,a=`${i}.tmp`;e.logger.info(S(`[skill-storage] saving state (${Object.keys(t).length} skill(s)) to: ${i}`));try{await u(r(i),{recursive:!0}),await g(a,JSON.stringify(t,null,2),`utf-8`),await p(a,i),e.logger.info(S(`[skill-storage] state saved successfully`))}catch(t){e.logger.error(S(`[skill-storage] failed to save state: ${t instanceof Error?t.message:String(t)}`)),await X(a)}})}function Be(e,t,n){let r={...e},i=n??Date.now();for(let e of t){let t=r[e.dirName];t&&t.dir_hash===e.dirHash&&!Fe(t)?Pe(t,i)?r[e.dirName]={...t,skill_name:e.skillName,is_detected:!1}:r[e.dirName]={...t,skill_name:e.skillName}:r[e.dirName]={skill_name:e.skillName,dir_hash:e.dirHash,zip_hash:``,is_detected:!1,last_detect_ts:null}}let a=new Set(t.map(e=>e.dirName));for(let e of Object.keys(r))a.has(e)||delete r[e];return r}function Ve(e){if(e.tool_name!==`read`)return null;let t=e.tool_args;return!t||typeof t!=`object`||Array.isArray(t)?null:Object.values(t).find(e=>typeof e==`string`&&e.endsWith(`SKILL.md`))??null}async function He(e,t,i){let a=n(r(t));try{let t;i&&(t=je(e.runtime.agent.resolveAgentWorkspaceDir(e.config,i)));let n=(await Re(e,t))[a];return!n||!n.zip_hash?null:{skill_name:n.skill_name,skill_hash:n.zip_hash}}catch{return null}}function Ue(...e){return e.find(e=>typeof e==`string`)??``}function We(e){return typeof e==`string`?e.toUpperCase():`PERMIT`}function Ge(e){if(!(typeof e!=`string`||e.length===0))try{let t=JSON.parse(e);return t&&typeof t==`object`&&!Array.isArray(t)?t:void 0}catch{return}}function Ke(e){return{effect:We(e?.effect),denyOutput:Ue(e?.deny_output),rewriteOutput:Ue(e?.rewrite_output)}}function qe(e){let t=We(e.effect);if(t===`DENY`)return e.denyOutput?{action:`deny`,output:e.denyOutput,returnValue:{block:!0,blockReason:e.denyOutput||`Tool call blocked by detect service`}}:{action:`permit`,returnValue:void 0};if(t===`REWRITE`){if(!e.rewriteOutput)return{action:`permit`,returnValue:void 0};let t=Ge(e.rewriteOutput);return t?{action:`rewrite`,returnValue:{params:t}}:{action:`rewrite_invalid`,returnValue:void 0}}return{action:`permit`,returnValue:void 0}}async function Je(e,t,n,r,i){if(!await t.isHookEnabled(n)){e.logger.info(S(`[${n}] detect skipped: hook disabled by runtime config`));return}let a=Date.now(),o=Y(n,r,i);E(e,n,o,D(e));try{let t=Ke(await K(e,n,o));e.logger.info(S(`[${n}] detect effect observed: effect=${t.effect} (report-only hook)`))}catch(t){e.logger.error(S(`[${n}] detect request failed: ${t instanceof Error?t.message:String(t)}`))}finally{let t=Date.now()-a;e.logger.info(S(`[${n}] total_elapsed=${t}ms`))}}async function Ye(e,t,n,r){let i=Date.now();if(!await t.isHookEnabled(`before_tool_call`)){e.logger.info(S(`[before_tool_call] detect skipped: hook disabled by runtime config`));return}let a=Y(`before_tool_call`,n,r),o=Ve(a),s=a;if(o){let t=await He(e,o,r.agentId);t&&(s={...s,...t},e.logger.info(S(`[before_tool_call] skill read detected: skill=${t.skill_name} hash=${t.skill_hash.slice(0,12)}...`)))}E(e,`before_tool_call`,s,D(e));let c;try{let t=qe(Ke(await K(e,`before_tool_call`,s)));e.logger.info(S(`[before_tool_call] detect effect applied: ${JSON.stringify(t.returnValue)}`)),c=t.returnValue}catch(t){e.logger.error(S(`[before_tool_call] detect request failed: ${t instanceof Error?t.message:String(t)}`))}let l=Date.now()-i;return e.logger.info(S(`[before_tool_call] total_elapsed=${l}ms`)),c}function Xe(e,t){e.on(`llm_input`,async(n,r)=>{await Je(e,t,`llm_input`,n,r)}),e.on(`llm_output`,async(n,r)=>{await Je(e,t,`llm_output`,n,r)}),e.on(`after_tool_call`,async(n,r)=>{await Je(e,t,`after_tool_call`,n,r)}),e.on(`before_tool_call`,async(n,r)=>Ye(e,t,n,r))}function Z(e){return typeof e==`object`&&!!e}function Ze(e,t){if(!Z(e))return null;let n=e[t];return Z(n)?n:null}function Qe(e){let t=Ze(Ze(e,`models`),`providers`);return t?Object.entries(t).map(([e,t])=>{if(!Z(t))return null;let n=t.baseUrl??t.baseURL;return typeof n==`string`&&n?{providerId:e,baseUrl:n}:null}).filter(e=>e!==null):[]}function $e(e,t){for(let n of t)if(e.startsWith(n.baseUrl))return n;return null}function et(e){return typeof e==`string`?e:e instanceof URLSearchParams?e.toString():e instanceof Uint8Array?new TextDecoder().decode(e):ArrayBuffer.isView(e)?new TextDecoder().decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)):e instanceof ArrayBuffer?new TextDecoder().decode(new Uint8Array(e)):null}function tt(e){let t=et(e);if(!t)return null;try{let e=JSON.parse(t);return Z(e)?e:null}catch{return null}}function nt(e){return Z(e)?Array.isArray(e.messages)||typeof e.prompt==`string`||typeof e.input==`string`:!1}function rt(e){return typeof e==`string`&&e.includes(`/open-apis/security_plugin/v1/openclaw_plugin`)}function it(e){return typeof e==`string`&&e.includes(`text/event-stream`)}function at(e,t){let n=e.split(/\r?\n/).filter(e=>e.startsWith(`data:`)).map(e=>e.slice(5).trim()).filter(e=>e.length>0&&e!==`[DONE]`),r=Math.max(1,Number(t)||1);if(r===1)return n;let i=[];for(let e=0;e<n.length;e+=r)i.push(n.slice(e,e+r).join(`
|
|
2
|
+
`));return i}function ot(e,t){return t?`${String(e).split(/\r?\n/).map(e=>`data: ${e}`).join(`
|
|
3
|
+
`)}\n\n`:e}function st(e,t,n){let r=new Headers(e.headers);return r.delete(`content-length`),n?r.set(`content-type`,`text/event-stream; charset=utf-8`):r.get(`content-type`)||r.set(`content-type`,`text/plain; charset=utf-8`),new Response(ot(t,n),{status:e.status,statusText:e.statusText,headers:r})}async function ct(e,t,n){let r=await t.clone().text(),i=it(t.headers.get(`content-type`)??``);return{url:e,isSse:i,chunks:i?at(r,n):[],originResp:i?``:r}}function lt(...e){return e.find(e=>typeof e==`string`)??``}function ut(e){return typeof e==`string`?e.toUpperCase():`PERMIT`}function dt(e){return{effect:ut(e?.effect),denyOutput:lt(e?.deny_output),rewriteOutput:lt(e?.rewrite_output)}}function ft(e){let t=ut(e.effect);return t===`DENY`&&e.denyOutput?{action:`deny`,output:e.denyOutput,effect:t}:t===`REWRITE`&&e.rewriteOutput?{action:`rewrite`,output:e.rewriteOutput,effect:t}:{action:`permit`,output:``,effect:t}}function pt(e){let t=ut(e.effect);return t===`DENY`&&e.denyOutput?{action:`deny`,output:e.denyOutput,effect:t}:t===`REWRITE`&&e.rewriteOutput?{action:`rewrite`,output:e.rewriteOutput,effect:t}:{action:`permit`,output:``,effect:t}}function mt(e,t,n){e.logger.info(S(`[${t}] ${T(n,D(e))}`))}function ht(){let e=globalThis.crypto;return e?.randomUUID?e.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`}function gt(e){let t=String(e);function n(e){if(typeof e!=`string`)return JSON.stringify(e);try{let t=JSON.parse(e);return typeof t==`string`?t:JSON.stringify(t)}catch{return e}}try{let e=JSON.parse(t);if(Array.isArray(e))return e.map(e=>n(e))}catch{}return t.split(/\r?\n/)}function _t(e,t){let n=new Headers(e.headers);n.delete(`content-length`),n.set(`content-type`,`text/event-stream; charset=utf-8`);let r=`${gt(t).map(e=>`data: ${e}\n\n`).join(``)}`;return new Response(r,{status:e.status,statusText:e.statusText,headers:n})}function vt(e){return(e.headers.get(`content-type`)??``).includes(`text/event-stream`)}async function yt(e,t,n,r,i,a){let o=r.body;if(!o)return r;let s=Math.max(1,Number(await i.getSseChunkSize())||1),c=o.getReader(),l=new TextDecoder,u=new TextEncoder,d=``,f=[],p=[];async function m(r){let i=Y(`after_llm_fetch`,null,null,{request_id:n,url:t,isSse:!0,chunks:r,originResp:``});return mt(e,`after_llm_fetch`,i),pt(dt(await K(e,`after_llm_fetch`,i)))}function h(e){f.length!==0&&(e.enqueue(u.encode(f.join(``))),f=[])}let g=new ReadableStream({async start(t){try{for(;;){let{value:i,done:o}=await c.read();if(o)break;for(d+=l.decode(i,{stream:!0});;){let i=d.indexOf(`
|
|
4
|
+
|
|
5
|
+
`),o=2;if(i<0&&(i=d.indexOf(`\r
|
|
6
|
+
\r
|
|
7
|
+
`),o=4),i<0)break;let l=d.slice(0,i);d=d.slice(i+o);let g=`${l}\n\n`;f.push(g);let _=l.split(/\r?\n/);for(let e of _){if(!e.startsWith(`data:`))continue;let t=e.slice(5).trim();!t||t===`[DONE]`||p.push(t)}if(p.length>=s){try{let i=await m(p);if((i.action===`deny`||i.action===`rewrite`)&&i.output){f=[],p=[];let o=await _t(r,i.output).text();t.enqueue(u.encode(o));try{await c.cancel()}catch{}t.close(),e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-a}ms request_id=${n}`));return}}catch(t){e.logger.error(S(`[after_llm_fetch] detect failed; passthrough buffered events: ${t instanceof Error?t.message:String(t)}`))}h(t),p=[]}}}if(d&&=(f.push(d),``),p.length>0)try{let i=await m(p);if((i.action===`deny`||i.action===`rewrite`)&&i.output){f=[],p=[];let o=await _t(r,i.output).text();t.enqueue(u.encode(o));try{await c.cancel()}catch{}t.close(),e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-a}ms request_id=${n}`));return}}catch(t){e.logger.error(S(`[after_llm_fetch] detect failed; passthrough buffered events: ${t instanceof Error?t.message:String(t)}`))}h(t),t.close(),e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-a}ms request_id=${n}`))}catch(e){try{await c.cancel()}catch{}t.error(e)}}}),_=new Headers(r.headers);return _.delete(`content-length`),_.set(`content-type`,`text/event-stream; charset=utf-8`),new Response(g,{status:r.status,statusText:r.statusText,headers:_})}function bt(e,t){let n=globalThis.fetch;if(typeof n!=`function`){e.logger.warn(S(`[before_llm_fetch] global.fetch is unavailable; skipping interceptor`));return}globalThis.__openclawGuardianOriginalFetch||(globalThis.__openclawGuardianOriginalFetch=n,globalThis.fetch=async function(r,i){let a=[r,i],o=r?.toString?.()??``,s=i??{},c=()=>n.call(globalThis,a[0],a[1]);if(a[1]||=s,rt(o))return c();let l,u=null;try{let r=$e(o,Qe(e.config));if(!r)return e.logger.info(S(`[before_llm_fetch] pass-through: no provider baseUrl match for url=${o}`)),c();e.logger.info(S(`[before_llm_fetch] matched provider=${r.providerId} baseUrl=${r.baseUrl} url=${o}`));let i=tt(s.body);if(!nt(i))return e.logger.info(S(`[before_llm_fetch] pass-through: matched provider=${r.providerId} but request body is not a supported LLM payload shape`)),c();let d={request_id:ht(),url:o,rawBody:et(s.body)??``,jsonBody:i},f=String(d.request_id);if(u=Date.now(),await t.isHookEnabled(`before_llm_fetch`)){let t=Y(`before_llm_fetch`,null,null,d);mt(e,`before_llm_fetch`,t);let n=dt(await K(e,`before_llm_fetch`,t)),i=ft(n);(i.action===`deny`||i.action===`rewrite`)&&i.output?(s.body=String(i.output),a[1]=s,e.logger.info(S(`[before_llm_fetch] detect effect applied: ${i.action} (use output as new body) provider=${r.providerId}`))):e.logger.info(S(`[before_llm_fetch] detect effect passthrough: effect=${n.effect} provider=${r.providerId}`))}else e.logger.info(S(`[before_llm_fetch] detect skipped: hook disabled by runtime config`));if(l=await n.apply(this,a),!await t.isHookEnabled(`after_llm_fetch`))return u!==null&&e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-u}ms request_id=${f}`)),l;if(vt(l))return await yt(e,o,f,l,t,u??Date.now());let p=await ct(o,l,await t.getSseChunkSize()),m=Y(`after_llm_fetch`,null,null,{...p,request_id:f});mt(e,`after_llm_fetch`,m);let h=dt(await K(e,`after_llm_fetch`,m)),g=pt(h);return(g.action===`deny`||g.action===`rewrite`)&&g.output?(e.logger.info(S(`[after_llm_fetch] detect effect applied: ${g.action} provider=${r.providerId}`)),u!==null&&e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-u}ms url=${o}`)),st(l,g.output,p.isSse)):(e.logger.info(S(`[after_llm_fetch] detect effect passthrough: effect=${h.effect} provider=${r.providerId}`)),u!==null&&e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-u}ms request_id=${f}`)),l)}catch(t){return e.logger.error(S(`[llm_fetch] detect request failed, allowing original fetch flow: ${t instanceof Error?t.message:String(t)}`)),l?(u!==null&&e.logger.info(S(`[llm_fetch] total_elapsed=${Date.now()-u}ms request_id=<unknown>`)),l):c()}})}var xt=y(((e,t)=>{function n(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,`default`)?e.default:e}let r=new Int32Array([0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,936918e3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117]);function i(e){if(Buffer.isBuffer(e))return e;if(typeof e==`number`)return Buffer.alloc(e);if(typeof e==`string`)return Buffer.from(e);throw Error(`input must be buffer, number, or string, received `+typeof e)}function a(e){let t=i(4);return t.writeInt32BE(e,0),t}function o(e,t){e=i(e),Buffer.isBuffer(t)&&(t=t.readUInt32BE(0));let n=~~t^-1;for(var a=0;a<e.length;a++)n=r[(n^e[a])&255]^n>>>8;return n^-1}function s(){return a(o.apply(null,arguments))}s.signed=function(){return o.apply(null,arguments)},s.unsigned=function(){return o.apply(null,arguments)>>>0},t.exports=n(s)})),St=y((e=>{var t=b(`fs`),n=b(`stream`).Transform,r=b(`stream`).PassThrough,i=b(`zlib`),a=b(`util`),o=b(`events`).EventEmitter,s=b(`events`).errorMonitor,c=xt();e.ZipFile=l,a.inherits(l,o);function l(){this.outputStream=new r,this.entries=[],this.outputStreamCursor=0,this.ended=!1,this.allDone=!1,this.forceZip64Eocd=!1,this.errored=!1,this.on(s,function(){this.errored=!0})}l.prototype.addFile=function(e,n,r){var i=this;if(n=S(n,!1),r??={},!g(i)){var a=new w(n,!1,r);i.entries.push(a),t.stat(e,function(n,o){if(n)return i.emit(`error`,n);if(!o.isFile())return i.emit(`error`,Error(`not a file: `+e));a.uncompressedSize=o.size,r.mtime??a.setLastModDate(o.mtime),r.mode??a.setFileAttributesMode(o.mode),a.setFileDataPumpFunction(function(){var n=t.createReadStream(e);a.state=w.FILE_DATA_IN_PROGRESS,n.on(`error`,function(e){i.emit(`error`,e)}),f(i,a,n)}),m(i)})}},l.prototype.addReadStream=function(e,t,n){this.addReadStreamLazy(t,n,function(t){t(null,e)})},l.prototype.addReadStreamLazy=function(e,t,n){var r=this;if(typeof t==`function`&&(n=t,t=null),t??={},e=S(e,!1),!g(r)){var i=new w(e,!1,t);r.entries.push(i),i.setFileDataPumpFunction(function(){i.state=w.FILE_DATA_IN_PROGRESS,n(function(e,t){if(e)return r.emit(`error`,e);f(r,i,t)})}),m(r)}},l.prototype.addBuffer=function(e,t,n){var r=this;if(t=S(t,!1),e.length>1073741823)throw Error(`buffer too large: `+e.length+` > 1073741823`);if(n??={},n.size!=null)throw Error(`options.size not allowed`);if(g(r))return;var a=new w(t,!1,n);a.uncompressedSize=e.length,a.crc32=c.unsigned(e),a.crcAndFileSizeKnown=!0,r.entries.push(a),a.compressionLevel===0?o(e):i.deflateRaw(e,{level:a.compressionLevel},function(e,t){o(t)});function o(e){a.compressedSize=e.length,a.setFileDataPumpFunction(function(){d(r,e),d(r,a.getDataDescriptor()),a.state=w.FILE_DATA_DONE,setImmediate(function(){m(r)})}),m(r)}},l.prototype.addEmptyDirectory=function(e,t){var n=this;if(e=S(e,!0),t??={},t.size!=null)throw Error(`options.size not allowed`);if(t.compress!=null)throw Error(`options.compress not allowed`);if(t.compressionLevel!=null)throw Error(`options.compressionLevel not allowed`);if(!g(n)){var r=new w(e,!0,t);n.entries.push(r),r.setFileDataPumpFunction(function(){d(n,r.getDataDescriptor()),r.state=w.FILE_DATA_DONE,m(n)}),m(n)}};var u=H([80,75,5,6]);l.prototype.end=function(e,t){if(typeof e==`function`&&(t=e,e=null),e??={},!this.ended&&(this.ended=!0,!this.errored)){if(this.calculatedTotalSizeCallback=t,this.forceZip64Eocd=!!e.forceZip64Format,e.comment){if(typeof e.comment==`string`?this.comment=re(e.comment):this.comment=e.comment,this.comment.length>65535)throw Error(`comment is too large`);if(U(this.comment,u))throw Error(`comment contains end of central directory record signature`)}else this.comment=C;m(this)}};function d(e,t){e.outputStream.write(t),e.outputStreamCursor+=t.length}function f(e,t,n){var a=new R,o=new L,s=t.compressionLevel===0?new r:new i.DeflateRaw({level:t.compressionLevel}),c=new L;n.pipe(a).pipe(o).pipe(s).pipe(c).pipe(e.outputStream,{end:!1}),c.on(`end`,function(){if(t.crc32=a.crc32,t.uncompressedSize==null)t.uncompressedSize=o.byteCount;else if(t.uncompressedSize!==o.byteCount)return e.emit(`error`,Error(`file data stream has unexpected number of bytes`));t.compressedSize=c.byteCount,e.outputStreamCursor+=t.compressedSize,d(e,t.getDataDescriptor()),t.state=w.FILE_DATA_DONE,m(e)})}function p(e){if(e.compress!=null&&e.compressionLevel!=null&&!!e.compress!=!!e.compressionLevel)throw Error(`conflicting settings for compress and compressionLevel`);return e.compressionLevel==null?e.compress===!1?0:6:e.compressionLevel}function m(e){if(e.allDone||e.errored)return;if(e.ended&&e.calculatedTotalSizeCallback!=null){var t=h(e);t!=null&&(e.calculatedTotalSizeCallback(t),e.calculatedTotalSizeCallback=null)}var n=r();function r(){for(var t=0;t<e.entries.length;t++){var n=e.entries[t];if(n.state<w.FILE_DATA_DONE)return n}return null}if(n!=null){if(n.state<w.READY_TO_PUMP_FILE_DATA||n.state===w.FILE_DATA_IN_PROGRESS)return;n.relativeOffsetOfLocalHeader=e.outputStreamCursor,d(e,n.getLocalFileHeader()),n.doFileDataPump()}else e.ended&&(e.offsetOfStartOfCentralDirectory=e.outputStreamCursor,e.entries.forEach(function(t){d(e,t.getCentralDirectoryRecord())}),d(e,x(e)),e.outputStream.end(),e.allDone=!0)}function h(e){for(var t=0,n=0,r=0;r<e.entries.length;r++){var i=e.entries[r];if(i.compressionLevel!==0)return-1;if(i.state>=w.READY_TO_PUMP_FILE_DATA){if(i.uncompressedSize==null)return-1}else if(i.uncompressedSize==null)return null;i.relativeOffsetOfLocalHeader=t;var a=i.useZip64Format();t+=T+i.utf8FileName.length,t+=i.uncompressedSize,i.crcAndFileSizeKnown||(a?t+=j:t+=A),n+=M+i.utf8FileName.length+i.fileComment.length,i.forceDosTimestamp||(n+=N),a&&(n+=P)}var o=0;return(e.forceZip64Eocd||e.entries.length>=65535||n>=65535||t>=4294967295)&&(o+=_+v),o+=y+e.comment.length,t+n+o}function g(e){if(e.ended)throw Error(`cannot add entries after calling end()`);return!!e.errored}var _=56,v=20,y=22;function x(e,t){var n=!1,r=e.entries.length;(e.forceZip64Eocd||e.entries.length>=65535)&&(r=65535,n=!0);var i=e.outputStreamCursor-e.offsetOfStartOfCentralDirectory,a=i;(e.forceZip64Eocd||i>=4294967295)&&(a=4294967295,n=!0);var o=e.offsetOfStartOfCentralDirectory;if((e.forceZip64Eocd||e.offsetOfStartOfCentralDirectory>=4294967295)&&(o=4294967295,n=!0),t)return n?_+v+y:y;var s=V(y+e.comment.length);if(s.writeUInt32LE(101010256,0),s.writeUInt16LE(0,4),s.writeUInt16LE(0,6),s.writeUInt16LE(r,8),s.writeUInt16LE(r,10),s.writeUInt32LE(a,12),s.writeUInt32LE(o,16),s.writeUInt16LE(e.comment.length,20),e.comment.copy(s,22),!n)return s;var c=V(_);c.writeUInt32LE(101075792,0),I(c,_-12,4),c.writeUInt16LE(D,12),c.writeUInt16LE(ee,14),c.writeUInt32LE(0,16),c.writeUInt32LE(0,20),I(c,e.entries.length,24),I(c,e.entries.length,32),I(c,i,40),I(c,e.offsetOfStartOfCentralDirectory,48);var l=V(v);return l.writeUInt32LE(117853008,0),l.writeUInt32LE(0,4),I(l,e.outputStreamCursor,8),l.writeUInt32LE(1,16),Buffer.concat([c,l,s])}function S(e,t){if(e===``)throw Error(`empty metadataPath`);if(e=e.replace(/\\/g,`/`),/^[a-zA-Z]:/.test(e)||/^\//.test(e))throw Error(`absolute path: `+e);if(e.split(`/`).indexOf(`..`)!==-1)throw Error(`invalid relative path: `+e);var n=/\/$/.test(e);if(t)n||(e+=`/`);else if(n)throw Error(`file path cannot end with '/': `+e);return e}var C=V(0);function w(e,t,n){if(this.utf8FileName=H(e),this.utf8FileName.length>65535)throw Error(`utf8 file name too long. `+utf8FileName.length+` > 65535`);if(this.isDirectory=t,this.state=w.WAITING_FOR_METADATA,this.setLastModDate(n.mtime==null?new Date:n.mtime),this.forceDosTimestamp=!!n.forceDosTimestamp,n.mode==null?this.setFileAttributesMode(t?16893:33204):this.setFileAttributesMode(n.mode),t?(this.crcAndFileSizeKnown=!0,this.crc32=0,this.uncompressedSize=0,this.compressedSize=0):(this.crcAndFileSizeKnown=!1,this.crc32=null,this.uncompressedSize=null,this.compressedSize=null,n.size!=null&&(this.uncompressedSize=n.size)),t?this.compressionLevel=0:this.compressionLevel=p(n),this.forceZip64Format=!!n.forceZip64Format,n.fileComment){if(typeof n.fileComment==`string`?this.fileComment=H(n.fileComment,`utf-8`):this.fileComment=n.fileComment,this.fileComment.length>65535)throw Error(`fileComment is too large`)}else this.fileComment=C}w.WAITING_FOR_METADATA=0,w.READY_TO_PUMP_FILE_DATA=1,w.FILE_DATA_IN_PROGRESS=2,w.FILE_DATA_DONE=3,w.prototype.setLastModDate=function(e){this.mtime=e;var t=ne(e);this.lastModFileTime=t.time,this.lastModFileDate=t.date},w.prototype.setFileAttributesMode=function(e){if((e&65535)!==e)throw Error(`invalid mode. expected: 0 <= `+e+` <= 65535`);this.externalFileAttributes=e<<16>>>0},w.prototype.setFileDataPumpFunction=function(e){this.doFileDataPump=e,this.state=w.READY_TO_PUMP_FILE_DATA},w.prototype.useZip64Format=function(){return this.forceZip64Format||this.uncompressedSize!=null&&this.uncompressedSize>4294967294||this.compressedSize!=null&&this.compressedSize>4294967294||this.relativeOffsetOfLocalHeader!=null&&this.relativeOffsetOfLocalHeader>4294967294};var T=30,E=20,ee=45,D=831,O=2048,k=8;w.prototype.getLocalFileHeader=function(){var e=0,t=0,n=0;this.crcAndFileSizeKnown&&(e=this.crc32,t=this.compressedSize,n=this.uncompressedSize);var r=V(T),i=O;return this.crcAndFileSizeKnown||(i|=k),r.writeUInt32LE(67324752,0),r.writeUInt16LE(E,4),r.writeUInt16LE(i,6),r.writeUInt16LE(this.getCompressionMethod(),8),r.writeUInt16LE(this.lastModFileTime,10),r.writeUInt16LE(this.lastModFileDate,12),r.writeUInt32LE(e,14),r.writeUInt32LE(t,18),r.writeUInt32LE(n,22),r.writeUInt16LE(this.utf8FileName.length,26),r.writeUInt16LE(0,28),Buffer.concat([r,this.utf8FileName])};var A=16,j=24;w.prototype.getDataDescriptor=function(){if(this.crcAndFileSizeKnown)return C;if(this.useZip64Format()){var e=V(j);return e.writeUInt32LE(134695760,0),e.writeUInt32LE(this.crc32,4),I(e,this.compressedSize,8),I(e,this.uncompressedSize,16),e}else{var e=V(A);return e.writeUInt32LE(134695760,0),e.writeUInt32LE(this.crc32,4),e.writeUInt32LE(this.compressedSize,8),e.writeUInt32LE(this.uncompressedSize,12),e}};var M=46,N=9,P=28;w.prototype.getCentralDirectoryRecord=function(){var e=V(M),t=O;this.crcAndFileSizeKnown||(t|=k);var n=C;if(!this.forceDosTimestamp){n=V(N),n.writeUInt16LE(21589,0),n.writeUInt16LE(N-4,2),n.writeUInt8(3,4);var r=Math.floor(this.mtime.getTime()/1e3);r<-2147483648&&(r=-2147483648),r>2147483647&&(r=2147483647),n.writeUInt32LE(r,5)}var i=this.compressedSize,a=this.uncompressedSize,o=this.relativeOffsetOfLocalHeader,s=E,c=C;return this.useZip64Format()&&(i=4294967295,a=4294967295,o=4294967295,s=ee,c=V(P),c.writeUInt16LE(1,0),c.writeUInt16LE(P-4,2),I(c,this.uncompressedSize,4),I(c,this.compressedSize,12),I(c,this.relativeOffsetOfLocalHeader,20)),e.writeUInt32LE(33639248,0),e.writeUInt16LE(D,4),e.writeUInt16LE(s,6),e.writeUInt16LE(t,8),e.writeUInt16LE(this.getCompressionMethod(),10),e.writeUInt16LE(this.lastModFileTime,12),e.writeUInt16LE(this.lastModFileDate,14),e.writeUInt32LE(this.crc32,16),e.writeUInt32LE(i,20),e.writeUInt32LE(a,24),e.writeUInt16LE(this.utf8FileName.length,28),e.writeUInt16LE(n.length+c.length,30),e.writeUInt16LE(this.fileComment.length,32),e.writeUInt16LE(0,34),e.writeUInt16LE(0,36),e.writeUInt32LE(this.externalFileAttributes,38),e.writeUInt32LE(o,42),Buffer.concat([e,this.utf8FileName,n,c,this.fileComment])},w.prototype.getCompressionMethod=function(){return this.compressionLevel===0?0:8};var F=new Date(1980,0,1),te=new Date(2107,11,31,23,59,58);function ne(e){e<F?e=F:e>te&&(e=te);var t=0;t|=e.getDate()&31,t|=(e.getMonth()+1&15)<<5,t|=(e.getFullYear()-1980&127)<<9;var n=0;return n|=Math.floor(e.getSeconds()/2),n|=(e.getMinutes()&63)<<5,n|=(e.getHours()&31)<<11,{date:t,time:n}}function I(e,t,n){var r=Math.floor(t/4294967296),i=t%4294967296;e.writeUInt32LE(i,n),e.writeUInt32LE(r,n+4)}a.inherits(L,n);function L(e){n.call(this,e),this.byteCount=0}L.prototype._transform=function(e,t,n){this.byteCount+=e.length,n(null,e)},a.inherits(R,n);function R(e){n.call(this,e),this.crc32=0}R.prototype._transform=function(e,t,n){this.crc32=c.unsigned(e,this.crc32),n(null,e)};var z=`\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\xA0`;if(z.length!==256)throw Error(`assertion failure`);var B=null;function re(e){if(/^[\x20-\x7e]*$/.test(e))return H(e,`utf-8`);if(B==null){B={};for(var t=0;t<z.length;t++)B[z[t]]=t}for(var n=V(e.length),t=0;t<e.length;t++){var r=B[e[t]];if(r==null)throw Error(`character not encodable in CP437: `+JSON.stringify(e[t]));n[t]=r}return n}function V(e){V=t;try{return V(e)}catch{return V=n,V(e)}function t(e){return Buffer.allocUnsafe(e)}function n(e){return new Buffer(e)}}function H(e,t){H=n;try{return H(e,t)}catch{return H=r,H(e,t)}function n(e,t){return Buffer.from(e,t)}function r(e,t){return new Buffer(e,t)}}function U(e,t){U=n;try{return U(e,t)}catch{return U=r,U(e,t)}function n(e,t){return e.includes(t)}function r(e,t){for(var n=0;n<=e.length-t.length;n++)for(var r=0;;r++){if(r===t.length)return!0;if(e[n+r]!==t[r])break}return!1}}}))();function Ct(e){if(!e||typeof e!=`object`||Array.isArray(e))return{};let t={};for(let[n,r]of Object.entries(e))t[n]=r===!0;return t}function wt(e){return{skills:Object.values(e).map(e=>({skill_name:e.skill_name,skill_hash:e.zip_hash}))}}async function Tt(e,t){let n=le(e),r=wt(t),i=await G(e,`batch_check_detection`,n,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r)},H);return i&&typeof i==`object`&&!Array.isArray(i)&&`result`in i?Ct(i.result):Ct(i)}const Et=10*1024*1024;async function Dt(e){let t=await f(e),n=[];for(let r of t){let t=i(e,r),a=await l(t);a.isSymbolicLink()||(a.isDirectory()?n.push(...await Dt(t)):a.isFile()&&n.push(t))}return n}async function Ot(e){let t=await m(e).catch(()=>null);if(!t||!t.isDirectory())throw Error(`zipDirectory: path does not exist or is not a directory: ${e}`);let n=await Dt(e);return n.sort((t,n)=>{let r=a(e,t),i=a(e,n);return r<i?-1:+(r>i)}),new Promise((t,r)=>{let i=new St.ZipFile,o=[],s=0,c=!1,l=e=>{c||(c=!0,r(e))};i.outputStream.on(`data`,t=>{if(!c){if(s+=t.length,s>Et){l(Error(`zipDirectory: zip exceeds ${Et} bytes limit for ${e} (${s} bytes)`));return}o.push(t)}}),i.outputStream.on(`end`,()=>{c||(c=!0,t(Buffer.concat(o)))}),i.outputStream.on(`error`,l);for(let t of n)i.addFile(t,a(e,t));i.end()})}async function kt(e,t,n){let r=ue(e),i=`skill_detect:${n.skill_name}`;e.logger.info(S(`[skill-detect] detect: sending skill=${n.skill_name} hash=${n.skill_hash.slice(0,12)}... dir=${t}`));let a=await Ot(t);e.logger.info(S(`[skill-detect] zipped ${t} (${a.length} bytes)`));let o=new FormData,s=new Blob([new Uint8Array(a)],{type:`application/zip`});return o.append(`file`,s,n.skill_name),o.append(`skill`,JSON.stringify({skill_name:n.skill_name,skill_hash:n.skill_hash})),o.append(`payload`,JSON.stringify(ve())),await G(e,i,r,{method:`POST`,body:o},6e5),e.logger.info(S(`[skill-detect] skill ${n.skill_name} detected successfully`)),!0}function Q(e){return e.replace(/\0/g,``)}function At(e,t){let n=s(),r=new Set,a=e.config??{},c=a?.skills?.load?.extraDirs;if(Array.isArray(c))for(let e of c)typeof e==`string`&&e.trim()&&r.add(o(Q(e.trim())));r.add(i(n,`.openclaw`,`skills`)),r.add(i(n,`.agents`,`skills`));let l=new Set,u=a?.agents?.defaults?.workspace;if(typeof u==`string`&&u.trim()&&l.add(o(Q(u.trim()))),Array.isArray(a?.agents?.list))for(let e of a.agents.list)typeof e?.workspace==`string`&&e.workspace.trim()&&l.add(o(Q(e.workspace.trim())));for(let e of l)r.add(i(e,`skills`)),r.add(i(e,`.agents`,`skills`));let d,f=e.pluginConfig;if(typeof f?.openClawDir==`string`&&f.openClawDir.trim())d=o(Q(f.openClawDir.trim()));else try{let t=e.runtime?.state?.resolveStateDir?.();typeof t==`string`&&t.trim()&&(d=t)}catch{}d&&(r.add(i(d,`skills`)),r.add(i(d,`.agents`,`skills`)));let p=a?.plugins;if(p?.installs&&typeof p.installs==`object`){for(let e of Object.values(p.installs))if(typeof e?.installPath==`string`&&e.installPath.trim()){let t=i(Q(e.installPath.trim()),`skills`);r.add(o(t))}}let m=t??i(n,`.openclaw`,`workspace`);r.add(i(m,`.agents`,`skills`)),r.add(i(m,`skills`));let h=Array.from(r),g=h.filter(e=>v(e));return e.logger.info(S(`[skill-scan] resolved ${h.length} skill path(s), ${g.length} exist`)),g}async function jt(e){let t;try{t=await f(e)}catch{return[]}let n=[];for(let r of t){let t=i(e,r),a=await l(t);a.isSymbolicLink()||(a.isDirectory()?n.push(...await jt(t)):a.isFile()&&n.push(t))}return n}const Mt=10*1024*1024,Nt=10*1024*1024;async function Pt(e){let t=await jt(e);if(t.length===0)return _(`sha256`).update(`empty-skill-dir`).digest(`hex`);let n=t.map(t=>a(e,t)),r=n.map((e,t)=>t);r.sort((e,t)=>n[e]<n[t]?-1:+(n[e]>n[t]));let i=_(`sha256`),o=0;for(let a of r){let r=n[a];i.update(`${r.length}:${r}\n`);let s=await d(t[a]);if(o+=s.length,o>Mt)throw Error(`computeSkillDirHash: skill directory exceeds ${Mt} byte limit: ${e}`);i.update(`${s.length}:`),i.update(s),i.update(`
|
|
8
|
+
`)}return i.digest(`hex`)}async function Ft(e){let t=await jt(e),n=t.map(t=>a(e,t)),r=n.map((e,t)=>t);r.sort((e,t)=>n[e]<n[t]?-1:+(n[e]>n[t]));let i=r.map(e=>t[e]),o=r.map(e=>n[e]);return new Promise((t,n)=>{let r=new St.ZipFile,a=_(`sha256`),s=0,c=!1,l=e=>{c||(c=!0,n(e))};r.outputStream.on(`data`,t=>{if(!c){if(s+=t.length,s>Nt){l(Error(`computeZipHash: zip exceeds ${Nt} byte limit for ${e}`));return}a.update(t)}}),r.outputStream.on(`end`,()=>{c||(c=!0,t(a.digest(`hex`)))}),r.outputStream.on(`error`,e=>l(e));for(let e=0;e<i.length;e++)r.addFile(i[e],o[e]);r.end()})}function It(e){let t=/^name:\s*(.+)$/m.exec(e);return t?t[1].trim():null}function Lt(e){let t=/^---\n([\s\S]*?)\n---/.exec(e);return t?t[1]:``}async function Rt(e,t){let n;try{n=await f(t,{withFileTypes:!0,encoding:`utf8`})}catch{return[]}let r=[];for(let a of n){if(!a.isDirectory())continue;let n=i(t,a.name),o=i(n,`SKILL.md`);try{let e=It(Lt(await d(o,`utf-8`)))||a.name,t=await Pt(n);r.push({dirName:a.name,dirPath:n,filePath:o,skillName:e,dirHash:t})}catch(t){t instanceof Error&&t.message.includes(`byte limit`)&&e.logger.warn(S(`[skill-scan] skipping skill '${a.name}': ${t.message}`))}}return r}async function zt(e,t){let n=At(e,t);e.logger.info(S(`[skill-scan] scanning ${n.length} skill location(s)`));let r=new Map;for(let t of n){let n=await Rt(e,t);n.length>0&&e.logger.info(S(`[skill-scan] ${t}: ${n.length} skill(s)`));for(let e of n)r.set(e.skillName,e)}let i=Array.from(r.values());return e.logger.info(S(`[skill-scan] scan complete: ${i.length} unique skill(s) across all locations`)),i}function Bt(e,t,n,r,i){e[t]={...n,skill_name:r.skill_name,zip_hash:r.skill_hash,is_detected:i,last_detect_ts:Date.now()}}async function Vt(e,t){e.logger.info(S(`[skill-scan] starting skill scan`));let n=Date.now(),r=je(t),i=await zt(e,t);if(i.length===0){e.logger.info(S(`[skill-scan] no skills found, skipping detection`));return}let a=Be(await Re(e,r),i);e.logger.info(S(`[skill-scan] merged state: ${Object.keys(a).length} skill(s)`));let o=new Map(i.map(e=>[e.dirName,e])),s=[];for(let[t,n]of Object.entries(a)){let r=o.get(t);if(r)try{if(Fe(n)){e.logger.info(S(`[skill-scan] ${n.skill_name}: zip_hash missing, computing`));let i=await Ft(r.dirPath);a[t]={...n,zip_hash:i,is_detected:!1},s.push({dirName:t,entry:a[t],entity:{skill_name:n.skill_name,skill_hash:i}})}else n.is_detected||(e.logger.info(S(`[skill-scan] ${n.skill_name}: not detected, queuing`)),s.push({dirName:t,entry:n,entity:{skill_name:n.skill_name,skill_hash:n.zip_hash}}))}catch(t){e.logger.error(S(`[skill-scan] failed to compute zip_hash for ${n.skill_name}: ${t instanceof Error?t.message:String(t)}`))}}if(e.logger.info(S(`[skill-scan] ${s.length} skill(s) need detection`)),s.length===0){await ze(e,a,r),e.logger.info(S(`[skill-scan] skill scan complete (nothing to detect) total_elapsed=${Date.now()-n}ms scanned=${i.length}`));return}let c={};for(let{dirName:e}of s)c[e]=a[e];let l={},u=!1;try{l=await Tt(e,c),e.logger.info(S(`[skill-scan] batch_check result: ${JSON.stringify(l)}`))}catch(t){e.logger.error(S(`[skill-scan] batch_check_detection failed: ${t instanceof Error?t.message:String(t)}`)),u=!0}for(let{dirName:t,entry:n,entity:r}of s)l[r.skill_hash]===!0&&(Bt(a,t,n,r,!0),e.logger.info(S(`[skill-scan] ${r.skill_name}: already known by server (batch_check)`)));if(u){e.logger.warn(S(`[skill-scan] skipping individual detect calls: batch_check_detection was unreachable`)),await ze(e,a,r),e.logger.info(S(`[skill-scan] skill scan complete (partial — batch_check failed) total_elapsed=${Date.now()-n}ms scanned=${i.length}`));return}for(let{dirName:t,entry:n,entity:r}of s){if(l[r.skill_hash]===!0)continue;let n=o.get(t);if(n)try{let i=await kt(e,n.dirPath,r);Bt(a,t,a[t],r,i),e.logger.info(S(`[skill-scan] detect result for ${r.skill_name}: is_detected=${i}`))}catch(n){e.logger.error(S(`[skill-scan] detect failed for ${r.skill_name}: ${n instanceof Error?n.message:String(n)}`)),Bt(a,t,a[t],r,!1)}}await ze(e,a,r),e.logger.info(S(`[skill-scan] skill scan complete total_elapsed=${Date.now()-n}ms scanned=${i.length} to_detect=${s.length}`))}async function Ht(e,t,n,r){e.logger.info(S(`[gateway_start] gateway ready on port=${n.port} ctx=${JSON.stringify(r)}`))}function Ut(e,t){let n=null,r=!1,i=!1,a;async function o(){if(i)return;let s=await t.getSkillScanIntervalMs();e.logger.info(S(`[skill-scan-service] next scan in ${s/1e3}s`)),n=setTimeout(async()=>{if(!i){if(!await t.isSkillDetectEnabled()){e.logger.info(S(`[skill-scan-service] periodic scan skipped: skill_detect disabled`)),await o();return}if(r){e.logger.info(S(`[skill-scan-service] periodic scan skipped: previous scan still running`)),await o();return}e.logger.info(S(`[skill-scan-service] periodic scan triggered`)),r=!0,Vt(e,a).catch(t=>{e.logger.error(S(`[skill-scan-service] periodic scan failed: ${t instanceof Error?t.message:String(t)}`))}).finally(()=>{r=!1,o()})}},s),n.unref()}return{id:`openclaw-guardian-skill-scan`,async start(n){a=n.workspaceDir,e.logger.info(S(`[skill-scan-service] starting with workspaceDir=${a??`(default)`}`)),i=!1,await t.isSkillDetectEnabled()&&(e.logger.info(S(`[skill-scan-service] running initial skill scan`)),r=!0,Vt(e,a).catch(t=>{e.logger.error(S(`[skill-scan-service] initial scan failed: ${t instanceof Error?t.message:String(t)}`))}).finally(()=>{r=!1})),await o()},async stop(t){i=!0,n&&=(clearTimeout(n),null),e.logger.info(S(`[skill-scan-service] stopped`))}}}function Wt(e,t){e.on(`gateway_start`,async(n,r)=>{await Ht(e,t,n,r)}),e.registerService(Ut(e,t))}const Gt=[...[`before_tool_call`,`after_tool_call`,`before_llm_fetch`,`after_llm_fetch`,`llm_input`,`llm_output`],`skill_detect`];function Kt(){return Object.fromEntries(Gt.map(e=>[e,!1]))}function qt(e){let t=Kt(),n=e&&typeof e==`object`?e:{};for(let e of Gt)t[e]=!!n[e];return t}function Jt(e){return typeof e==`number`&&Number.isFinite(e)&&e>0?Math.floor(e):1}function Yt(e){return typeof e==`number`&&Number.isFinite(e)&&e>0?Math.floor(e):900}function $(e){return{enabled:!1,hook_toggles:Kt(),sse_chunk_size:1,skill_scan_duration_second:900,reason:e}}function Xt(e){let t=e&&typeof e==`object`?e:{},n=t.enabled===void 0?!0:!!t.enabled;return{enabled:n,hook_toggles:n?qt(t.hook_toggles):Kt(),sse_chunk_size:Jt(t.sse_chunk_size),skill_scan_duration_second:Yt(t.skill_scan_duration_second),reason:n?`remote`:`remote_disabled`}}function Zt(e){return Qe(e.config).map(e=>e.baseUrl)}async function Qt(e){let t={source:`miaoda`,base_url_list:Zt(e)};return Xt(await G(e,`runtime_config`,se(e),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(t)},H))}function $t(e){let t=$(`not_initialized`),n=!1,r=null,i=null;async function a(){let r=null;for(let i=1;i<=3;i+=1)try{let r=await Qt(e);return t=r,n=!0,e.logger.debug?.(S(`[runtime_config] initial config loaded on attempt=${i} enabled_hooks=${JSON.stringify(r.hook_toggles)}`)),t}catch(t){r=t;let n=V(t);if(e.logger.error(S(`[runtime_config] initial config load failed attempt=${i} errorCode=${n}: ${t instanceof Error?t.message:String(t)}`)),n===`SERVER`||n===`PARSE`)break}return t=$(`initial_fetch_failed`),e.logger.warn(S(`[runtime_config] initial config unavailable after 3 attempts; detection disabled by default${r?`: ${r instanceof Error?r.message:String(r)}`:``}`)),t}async function o(){try{let r=await Qt(e);return t=r,n=!0,e.logger.debug?.(S(`[runtime_config] config refreshed enabled_hooks=${JSON.stringify(r.hook_toggles)} sse_chunk_size=${r.sse_chunk_size}`)),t}catch(r){let i=V(r);return n?(e.logger.error(S(`[runtime_config] refresh failed (errorCode=${i}); keeping previous config: ${r instanceof Error?r.message:String(r)}`)),t):(e.logger.error(S(`[runtime_config] refresh failed before first successful load (errorCode=${i}): ${r instanceof Error?r.message:String(r)}`)),t=$(`refresh_failed_${i}`),t)}}function s(){r||(r=setInterval(()=>{e.logger.debug?.(S(`[runtime_config] refresh loop fired`)),o().catch(t=>{e.logger.error(S(`[runtime_config] refresh loop failed: ${t instanceof Error?t.message:String(t)}`))})},6e5),r.unref())}function c(){return i||=a().catch(n=>(e.logger.error(S(`[runtime_config] initialize failed: ${n instanceof Error?n.message:String(n)}`)),t)).finally(()=>{s()}),i}return{async isHookEnabled(e){return await c(),!!(t?.enabled&&t?.hook_toggles?.[e])},async isSkillDetectEnabled(){return await c(),!!(t?.enabled&&t?.hook_toggles?.skill_detect)},async getSseChunkSize(){return await c(),Jt(t?.sse_chunk_size)},async getSkillScanIntervalMs(){return await c(),Yt(t?.skill_scan_duration_second)*1e3}}}var en=t({id:`openclaw-guardian-plugin`,name:`OpenClaw Guardian Plugin`,description:`Reports OpenClaw hook payloads to the trust_layer detect service with remote hook toggles.`,register(e){if(!P(e)){e.logger.debug?.(S(`[plugin] missing Feishu appId/appSecret from api.config.channels.feishu or pluginConfig; skip all hooks and reporting`));return}let t=$t(e);bt(e,t),Xe(e,t),Wt(e,t)}});export{en as default};
|
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-tns/openclaw-guardian-plugin",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that reports hook payloads to the security_plugin detect service with remote hook toggles.",
|
|
6
6
|
"keywords": ["openclaw", "plugin", "security", "guardian"],
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"dist",
|
|
9
9
|
"openclaw.plugin.json",
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
+
"build": "tsdown",
|
|
13
14
|
"typecheck": "tsc --noEmit",
|
|
14
|
-
"
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"pack": "npm run build && npm pack",
|
|
15
17
|
"upload": "node scripts/uploadManifest.mjs"
|
|
16
18
|
},
|
|
17
19
|
"openclaw": {
|
|
18
20
|
"extensions": [
|
|
19
|
-
"./
|
|
20
|
-
]
|
|
21
|
-
"installDependencies": true
|
|
21
|
+
"./dist/index.js"
|
|
22
|
+
]
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/yazl": "^3.3.0",
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
"dependencies": {
|
|
26
|
+
"tsdown": "^0.12.5",
|
|
27
|
+
"typescript": "^5.8.3",
|
|
28
28
|
"yazl": "^3.3.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
@@ -34,5 +34,8 @@
|
|
|
34
34
|
"openclaw": {
|
|
35
35
|
"optional": false
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/src/common-payload.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { getHostname } from './hostname';
|
|
2
|
-
|
|
3
|
-
export type CommonPayloadFields = {
|
|
4
|
-
hostname: string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export function buildCommonPayloadFields(): CommonPayloadFields {
|
|
8
|
-
return {
|
|
9
|
-
hostname: getHostname(),
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function mergeCommonPayloadFields(payload: unknown): unknown {
|
|
14
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
15
|
-
return payload;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const record = payload as Record<string, unknown>;
|
|
19
|
-
if (Object.prototype.hasOwnProperty.call(record, 'hostname')) {
|
|
20
|
-
return payload;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
...record,
|
|
25
|
-
...buildCommonPayloadFields(),
|
|
26
|
-
};
|
|
27
|
-
}
|
package/src/detect-service.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_TIMEOUT_MS, fetchSecurityPluginApi, getDetectUrl, createDetectRequestBody } from './http-client';
|
|
2
|
-
import { mergeCommonPayloadFields } from './common-payload';
|
|
3
|
-
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
4
|
-
|
|
5
|
-
export async function detectHookPayload(api: OpenClawPluginApi, hookName: string, payload: unknown) {
|
|
6
|
-
const source = 'miaoda'; //TODO: Mock for now, should get from config later
|
|
7
|
-
const body = createDetectRequestBody(hookName, mergeCommonPayloadFields(payload), source);
|
|
8
|
-
|
|
9
|
-
return fetchSecurityPluginApi(api, hookName, getDetectUrl(api), {
|
|
10
|
-
method: 'POST',
|
|
11
|
-
headers: {
|
|
12
|
-
'content-type': 'application/json',
|
|
13
|
-
},
|
|
14
|
-
body: JSON.stringify(body),
|
|
15
|
-
}, DEFAULT_TIMEOUT_MS);
|
|
16
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { basename, dirname } from 'node:path';
|
|
2
|
-
import type {
|
|
3
|
-
PluginHookAfterToolCallEvent,
|
|
4
|
-
PluginHookAgentContext,
|
|
5
|
-
PluginHookBeforeToolCallEvent,
|
|
6
|
-
PluginHookBeforeToolCallResult,
|
|
7
|
-
PluginHookLlmInputEvent,
|
|
8
|
-
PluginHookLlmOutputEvent,
|
|
9
|
-
PluginHookToolContext,
|
|
10
|
-
} from 'openclaw/plugin-sdk/plugin-runtime';
|
|
11
|
-
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
12
|
-
import { detectHookPayload } from '../detect-service';
|
|
13
|
-
import { buildHookPayload } from '../hook-payloads';
|
|
14
|
-
import { logHookPayloadWithEnv, withLogPrefix } from '../logger';
|
|
15
|
-
import { getPluginEnv } from '../plugin-env';
|
|
16
|
-
import type { RuntimeConfigStore } from '../runtime-config';
|
|
17
|
-
import { loadSkillState, resolveStoragePath } from '../skill-scan/skill-storage';
|
|
18
|
-
|
|
19
|
-
type UnknownRecord = Record<string, unknown>;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Returns the SKILL.md file path if this tool call is a skill read:
|
|
23
|
-
* tool_name === "read" and tool_args.path ends with "SKILL.md".
|
|
24
|
-
*/
|
|
25
|
-
function extractSkillFilePath(payload: UnknownRecord): string | null {
|
|
26
|
-
if (payload.tool_name !== 'read') return null;
|
|
27
|
-
const args = payload.tool_args;
|
|
28
|
-
if (!args || typeof args !== 'object' || Array.isArray(args)) return null;
|
|
29
|
-
const values = Object.values(args as UnknownRecord);
|
|
30
|
-
return (values.find((v): v is string => typeof v === 'string' && v.endsWith('SKILL.md')) ?? null);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function resolveSkillMeta(
|
|
34
|
-
api: OpenClawPluginApi,
|
|
35
|
-
skillFilePath: string,
|
|
36
|
-
agentId?: string,
|
|
37
|
-
): Promise<{ skill_name: string; skill_hash: string } | null> {
|
|
38
|
-
// dirName is the cache key in skill-detect-state.json
|
|
39
|
-
const dirName = basename(dirname(skillFilePath));
|
|
40
|
-
try {
|
|
41
|
-
let storagePath: string | undefined;
|
|
42
|
-
if (agentId) {
|
|
43
|
-
const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
|
|
44
|
-
storagePath = resolveStoragePath(workspaceDir);
|
|
45
|
-
}
|
|
46
|
-
const state = await loadSkillState(api, storagePath);
|
|
47
|
-
const entry = state[dirName];
|
|
48
|
-
if (!entry || !entry.zip_hash) return null;
|
|
49
|
-
return { skill_name: entry.skill_name, skill_hash: entry.zip_hash };
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type DetectResponseLike = {
|
|
56
|
-
effect?: unknown;
|
|
57
|
-
deny_output?: unknown;
|
|
58
|
-
rewrite_output?: unknown;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type DetectDecision = {
|
|
62
|
-
effect: string;
|
|
63
|
-
denyOutput: string;
|
|
64
|
-
rewriteOutput: string;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
type ReportOnlyHookName = 'llm_input' | 'llm_output' | 'after_tool_call';
|
|
68
|
-
|
|
69
|
-
type ReportOnlyHookEventMap = {
|
|
70
|
-
llm_input: PluginHookLlmInputEvent;
|
|
71
|
-
llm_output: PluginHookLlmOutputEvent;
|
|
72
|
-
after_tool_call: PluginHookAfterToolCallEvent;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type ReportOnlyHookContextMap = {
|
|
76
|
-
llm_input: PluginHookAgentContext;
|
|
77
|
-
llm_output: PluginHookAgentContext;
|
|
78
|
-
after_tool_call: PluginHookToolContext;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function firstString(...values: unknown[]): string {
|
|
82
|
-
return values.find((value) => typeof value === 'string') ?? '';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function normalizeEffect(effect: unknown): string {
|
|
86
|
-
if (typeof effect !== 'string') {
|
|
87
|
-
return 'PERMIT';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return effect.toUpperCase();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function parseRewriteParams(text: string): UnknownRecord | undefined {
|
|
94
|
-
if (typeof text !== 'string' || text.length === 0) {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const parsed = JSON.parse(text);
|
|
100
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
101
|
-
? (parsed as UnknownRecord)
|
|
102
|
-
: undefined;
|
|
103
|
-
} catch {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function parseDetectDecision(response: DetectResponseLike | null | undefined): DetectDecision {
|
|
109
|
-
return {
|
|
110
|
-
effect: normalizeEffect(response?.effect),
|
|
111
|
-
denyOutput: firstString(response?.deny_output),
|
|
112
|
-
rewriteOutput: firstString(response?.rewrite_output),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function applyBeforeToolCallDecision(decision: DetectDecision) {
|
|
117
|
-
const effect = normalizeEffect(decision.effect);
|
|
118
|
-
|
|
119
|
-
if (effect === 'DENY') {
|
|
120
|
-
if (!decision.denyOutput) {
|
|
121
|
-
return {
|
|
122
|
-
action: 'permit',
|
|
123
|
-
returnValue: undefined,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
action: 'deny',
|
|
129
|
-
output: decision.denyOutput,
|
|
130
|
-
returnValue: {
|
|
131
|
-
block: true,
|
|
132
|
-
blockReason: decision.denyOutput || 'Tool call blocked by detect service',
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (effect === 'REWRITE') {
|
|
138
|
-
if (!decision.rewriteOutput) {
|
|
139
|
-
return {
|
|
140
|
-
action: 'permit',
|
|
141
|
-
returnValue: undefined,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const rewrittenParams = parseRewriteParams(decision.rewriteOutput);
|
|
146
|
-
if (!rewrittenParams) {
|
|
147
|
-
return {
|
|
148
|
-
action: 'rewrite_invalid',
|
|
149
|
-
returnValue: undefined,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
action: 'rewrite',
|
|
155
|
-
returnValue: {
|
|
156
|
-
params: rewrittenParams,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
action: 'permit',
|
|
163
|
-
returnValue: undefined,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function handleReportOnlyHook<K extends ReportOnlyHookName>(
|
|
168
|
-
api: OpenClawPluginApi,
|
|
169
|
-
configStore: RuntimeConfigStore,
|
|
170
|
-
hookName: K,
|
|
171
|
-
event: ReportOnlyHookEventMap[K],
|
|
172
|
-
ctx: ReportOnlyHookContextMap[K],
|
|
173
|
-
): Promise<void> {
|
|
174
|
-
if (!(await configStore.isHookEnabled(hookName))) {
|
|
175
|
-
api.logger.info(withLogPrefix(`[${hookName}] detect skipped: hook disabled by runtime config`));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const totalStart = Date.now();
|
|
180
|
-
const payload = buildHookPayload(hookName, event, ctx);
|
|
181
|
-
logHookPayloadWithEnv(api, hookName, payload, getPluginEnv(api));
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const detectResponse = await detectHookPayload(api, hookName, payload);
|
|
185
|
-
const decision = parseDetectDecision(detectResponse);
|
|
186
|
-
api.logger.info(
|
|
187
|
-
withLogPrefix(`[${hookName}] detect effect observed: effect=${decision.effect} (report-only hook)`),
|
|
188
|
-
);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
api.logger.error(
|
|
191
|
-
withLogPrefix(
|
|
192
|
-
`[${hookName}] detect request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
193
|
-
),
|
|
194
|
-
);
|
|
195
|
-
} finally {
|
|
196
|
-
const totalElapsed = Date.now() - totalStart;
|
|
197
|
-
api.logger.info(withLogPrefix(`[${hookName}] total_elapsed=${totalElapsed}ms`));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function handleBeforeToolCall(
|
|
202
|
-
api: OpenClawPluginApi,
|
|
203
|
-
configStore: RuntimeConfigStore,
|
|
204
|
-
event: PluginHookBeforeToolCallEvent,
|
|
205
|
-
ctx: PluginHookToolContext,
|
|
206
|
-
): Promise<PluginHookBeforeToolCallResult> {
|
|
207
|
-
const totalStart = Date.now();
|
|
208
|
-
if (!(await configStore.isHookEnabled('before_tool_call'))) {
|
|
209
|
-
api.logger.info(withLogPrefix('[before_tool_call] detect skipped: hook disabled by runtime config'));
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const payload = buildHookPayload('before_tool_call', event, ctx);
|
|
214
|
-
|
|
215
|
-
// Enrich payload with skill metadata if this is a skill read
|
|
216
|
-
const skillFilePath = extractSkillFilePath(payload as UnknownRecord);
|
|
217
|
-
let enrichedPayload: UnknownRecord = payload as UnknownRecord;
|
|
218
|
-
if (skillFilePath) {
|
|
219
|
-
const skillMeta = await resolveSkillMeta(api, skillFilePath, ctx.agentId);
|
|
220
|
-
if (skillMeta) {
|
|
221
|
-
enrichedPayload = { ...enrichedPayload, ...skillMeta };
|
|
222
|
-
api.logger.info(
|
|
223
|
-
withLogPrefix(
|
|
224
|
-
`[before_tool_call] skill read detected: skill=${skillMeta.skill_name} hash=${skillMeta.skill_hash.slice(0, 12)}...`,
|
|
225
|
-
),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
logHookPayloadWithEnv(api, 'before_tool_call', enrichedPayload, getPluginEnv(api));
|
|
231
|
-
|
|
232
|
-
let hookReturnValue: PluginHookBeforeToolCallResult = undefined;
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const detectResponse = await detectHookPayload(api, 'before_tool_call', enrichedPayload);
|
|
236
|
-
const decision = parseDetectDecision(detectResponse);
|
|
237
|
-
const outcome = applyBeforeToolCallDecision(decision);
|
|
238
|
-
api.logger.info(withLogPrefix(`[before_tool_call] detect effect applied: ${JSON.stringify(outcome.returnValue)}`));
|
|
239
|
-
hookReturnValue = outcome.returnValue;
|
|
240
|
-
} catch (error) {
|
|
241
|
-
api.logger.error(
|
|
242
|
-
withLogPrefix(
|
|
243
|
-
`[before_tool_call] detect request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
244
|
-
),
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const totalElapsed = Date.now() - totalStart;
|
|
249
|
-
api.logger.info(withLogPrefix(`[before_tool_call] total_elapsed=${totalElapsed}ms`));
|
|
250
|
-
return hookReturnValue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function registerDetectHooks(api: OpenClawPluginApi, configStore: RuntimeConfigStore): void {
|
|
254
|
-
api.on('llm_input', async (event, ctx) => {
|
|
255
|
-
await handleReportOnlyHook(api, configStore, 'llm_input', event, ctx);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
api.on('llm_output', async (event, ctx) => {
|
|
259
|
-
await handleReportOnlyHook(api, configStore, 'llm_output', event, ctx);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
api.on('after_tool_call', async (event, ctx) => {
|
|
263
|
-
await handleReportOnlyHook(api, configStore, 'after_tool_call', event, ctx);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
api.on('before_tool_call', async (event, ctx) => {
|
|
267
|
-
return handleBeforeToolCall(api, configStore, event, ctx);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ContextLike, FetchMeta, LlmEventLike, ToolEventLike } from './shared';
|
|
2
|
-
import { safeObject } from './shared';
|
|
3
|
-
import { buildAfterLlmFetchPayload, buildBeforeLlmFetchPayload } from './llm-fetch-payloads';
|
|
4
|
-
import { buildLlmInputPayload, buildLlmOutputPayload } from './llm-payloads';
|
|
5
|
-
import { buildAfterToolCallPayload, buildBeforeToolCallPayload } from './tool-payloads';
|
|
6
|
-
|
|
7
|
-
export type HookName =
|
|
8
|
-
| 'before_tool_call'
|
|
9
|
-
| 'after_tool_call'
|
|
10
|
-
| 'before_llm_fetch'
|
|
11
|
-
| 'after_llm_fetch'
|
|
12
|
-
| 'llm_input'
|
|
13
|
-
| 'llm_output';
|
|
14
|
-
|
|
15
|
-
export { buildAfterLlmFetchPayload, buildBeforeLlmFetchPayload } from './llm-fetch-payloads';
|
|
16
|
-
export { buildLlmInputPayload, buildLlmOutputPayload } from './llm-payloads';
|
|
17
|
-
export { buildAfterToolCallPayload, buildBeforeToolCallPayload } from './tool-payloads';
|
|
18
|
-
|
|
19
|
-
export function buildHookPayload(
|
|
20
|
-
hookName: HookName,
|
|
21
|
-
event: unknown,
|
|
22
|
-
ctx: unknown,
|
|
23
|
-
extra?: unknown,
|
|
24
|
-
) {
|
|
25
|
-
const toolEvent = event as ToolEventLike | null | undefined;
|
|
26
|
-
const llmEvent = event as LlmEventLike | null | undefined;
|
|
27
|
-
const safeContext = ctx as ContextLike | null | undefined;
|
|
28
|
-
|
|
29
|
-
switch (hookName) {
|
|
30
|
-
case 'before_tool_call':
|
|
31
|
-
return buildBeforeToolCallPayload(toolEvent, safeContext);
|
|
32
|
-
case 'after_tool_call':
|
|
33
|
-
return buildAfterToolCallPayload(toolEvent, safeContext);
|
|
34
|
-
case 'before_llm_fetch':
|
|
35
|
-
return buildBeforeLlmFetchPayload(extra as FetchMeta | null | undefined);
|
|
36
|
-
case 'after_llm_fetch':
|
|
37
|
-
return buildAfterLlmFetchPayload(extra as FetchMeta | null | undefined);
|
|
38
|
-
case 'llm_input':
|
|
39
|
-
return buildLlmInputPayload(llmEvent, safeContext);
|
|
40
|
-
case 'llm_output':
|
|
41
|
-
return buildLlmOutputPayload(llmEvent, safeContext);
|
|
42
|
-
default:
|
|
43
|
-
return safeObject(extra);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { FetchMeta } from './shared';
|
|
2
|
-
import { firstDefined, parseUrlParts, safeArray, safeString } from './shared';
|
|
3
|
-
|
|
4
|
-
export function buildBeforeLlmFetchPayload(meta: FetchMeta | null | undefined) {
|
|
5
|
-
const { domain, path } = parseUrlParts(safeString(meta?.url));
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
request_id: safeString(meta?.request_id),
|
|
9
|
-
domain,
|
|
10
|
-
path,
|
|
11
|
-
origin_req: firstDefined(meta?.jsonBody, meta?.rawBody, ''),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildAfterLlmFetchPayload(meta: FetchMeta | null | undefined) {
|
|
16
|
-
const { domain, path } = parseUrlParts(safeString(meta?.url));
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
request_id: safeString(meta?.request_id),
|
|
20
|
-
domain,
|
|
21
|
-
path,
|
|
22
|
-
is_sse: meta?.isSse ? 'true' : 'false',
|
|
23
|
-
chunks: safeArray(meta?.chunks),
|
|
24
|
-
origin_resp: meta?.isSse ? '' : safeString(firstDefined(meta?.originResp, '')),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { ContextLike, LlmEventLike } from './shared';
|
|
2
|
-
import { safeArray, safeString } from './shared';
|
|
3
|
-
|
|
4
|
-
function createLlmBasePayload(event: LlmEventLike | null | undefined, ctx: ContextLike | null | undefined) {
|
|
5
|
-
return {
|
|
6
|
-
agent_name: safeString(ctx?.agentId),
|
|
7
|
-
session_id: safeString(ctx?.sessionId),
|
|
8
|
-
session_key: safeString(ctx?.sessionKey),
|
|
9
|
-
channel_id: safeString(ctx?.channelId),
|
|
10
|
-
message_provider: safeString(ctx?.messageProvider),
|
|
11
|
-
trigger: safeString(ctx?.trigger),
|
|
12
|
-
model: safeString(event?.model),
|
|
13
|
-
provider: safeString(event?.provider),
|
|
14
|
-
run_id: safeString(event?.runId),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function buildLlmInputPayload(event: LlmEventLike | null | undefined, ctx: ContextLike | null | undefined) {
|
|
19
|
-
return {
|
|
20
|
-
...createLlmBasePayload(event, ctx),
|
|
21
|
-
prompt: safeString(event?.prompt),
|
|
22
|
-
system_prompt: safeString(event?.systemPrompt),
|
|
23
|
-
history_messages: safeArray(event?.historyMessages),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildLlmOutputPayload(event: LlmEventLike | null | undefined, ctx: ContextLike | null | undefined) {
|
|
28
|
-
return {
|
|
29
|
-
...createLlmBasePayload(event, ctx),
|
|
30
|
-
assistant_texts: safeArray(event?.assistantTexts),
|
|
31
|
-
last_assistant: safeString(JSON.stringify(event?.lastAssistant)),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export type UnknownRecord = Record<string, unknown>;
|
|
2
|
-
|
|
3
|
-
export type ToolEventLike = UnknownRecord & {
|
|
4
|
-
toolCallId?: unknown;
|
|
5
|
-
toolName?: unknown;
|
|
6
|
-
params?: unknown;
|
|
7
|
-
runId?: unknown;
|
|
8
|
-
result?: unknown;
|
|
9
|
-
error?: unknown;
|
|
10
|
-
durationMs?: unknown;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type LlmEventLike = UnknownRecord & {
|
|
14
|
-
model?: unknown;
|
|
15
|
-
provider?: unknown;
|
|
16
|
-
runId?: unknown;
|
|
17
|
-
prompt?: unknown;
|
|
18
|
-
systemPrompt?: unknown;
|
|
19
|
-
historyMessages?: unknown;
|
|
20
|
-
assistantTexts?: unknown;
|
|
21
|
-
lastAssistant?: unknown;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type ContextLike = UnknownRecord & {
|
|
25
|
-
agentId?: unknown;
|
|
26
|
-
sessionId?: unknown;
|
|
27
|
-
sessionKey?: unknown;
|
|
28
|
-
channelId?: unknown;
|
|
29
|
-
messageProvider?: unknown;
|
|
30
|
-
trigger?: unknown;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type FetchMeta = {
|
|
34
|
-
request_id?: unknown;
|
|
35
|
-
url?: unknown;
|
|
36
|
-
rawBody?: unknown;
|
|
37
|
-
jsonBody?: unknown;
|
|
38
|
-
isSse?: unknown;
|
|
39
|
-
chunks?: unknown;
|
|
40
|
-
originResp?: unknown;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export function firstDefined<T>(...values: Array<T | undefined | null>): T | undefined {
|
|
44
|
-
return values.find((value) => value !== undefined && value !== null);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function safeString(value: unknown): string {
|
|
48
|
-
if (value === undefined || value === null) {
|
|
49
|
-
return '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return typeof value === 'string' ? value : String(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function safeObject(value: unknown): UnknownRecord {
|
|
56
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
57
|
-
? (value as UnknownRecord)
|
|
58
|
-
: {};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function safeArray(value: unknown): unknown[] {
|
|
62
|
-
return Array.isArray(value) ? value : [];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function parseUrlParts(url: string): { domain: string; path: string } {
|
|
66
|
-
try {
|
|
67
|
-
const parsedUrl = new URL(url);
|
|
68
|
-
return {
|
|
69
|
-
domain: parsedUrl.host,
|
|
70
|
-
path: parsedUrl.pathname || '/',
|
|
71
|
-
};
|
|
72
|
-
} catch {
|
|
73
|
-
return {
|
|
74
|
-
domain: '',
|
|
75
|
-
path: '',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|