@robota-sdk/agent-core 3.0.0-beta.86 → 3.0.0-beta.88
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/CHANGELOG.md +36 -15
- package/README.md +56 -22
- package/dist/browser/{bounded-output-Bgx2Cgkx.js → bounded-output-BdWv9SX3.js} +2 -2
- package/dist/browser/bounded-output-BdWv9SX3.js.map +1 -0
- package/dist/browser/{command-executor-C0XXfBnt.js → command-executor-DIXaSMrr.js} +2 -2
- package/dist/{node/command-executor-d6DdfUTD.js.map → browser/command-executor-DIXaSMrr.js.map} +1 -1
- package/dist/browser/index.d.ts +140 -48
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +10 -9
- package/dist/browser/index.js.map +1 -1
- package/dist/node/{bounded-output-Bgx2Cgkx.js → bounded-output-BdWv9SX3.js} +2 -2
- package/dist/node/bounded-output-BdWv9SX3.js.map +1 -0
- package/dist/node/{command-executor-d6DdfUTD.js → command-executor-D_UBZHxh.js} +2 -2
- package/dist/{browser/command-executor-C0XXfBnt.js.map → node/command-executor-D_UBZHxh.js.map} +1 -1
- package/dist/node/{command-executor-CqAFpB-X.cjs → command-executor-wBgBdWXr.cjs} +1 -1
- package/dist/node/{http-executor-BFtXs8FP.cjs → http-executor-CeAAZotH.cjs} +1 -1
- package/dist/node/{index-CPwJEiFl.d.cts → index-CagopdzH.d.cts} +141 -49
- package/dist/node/index-CagopdzH.d.cts.map +1 -0
- package/dist/node/{index-DQrmrFn9.d.ts → index-CjyNzgQN.d.ts} +141 -49
- package/dist/node/index-CjyNzgQN.d.ts.map +1 -0
- package/dist/node/index.cjs +10 -9
- package/dist/node/index.d.cts +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +12 -11
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.cjs +1 -1
- package/dist/node/node.d.cts +13 -2
- package/dist/node/node.d.cts.map +1 -1
- package/dist/node/node.d.ts +13 -2
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +1 -1
- package/dist/node/node.js.map +1 -1
- package/dist/node/testing/index.d.cts +1 -1
- package/dist/node/testing/index.d.ts +1 -1
- package/dist/node/{verdict-decoder-DIHay1ul.cjs → verdict-decoder-DQqwwDE7.cjs} +1 -1
- package/package.json +10 -10
- package/dist/browser/bounded-output-Bgx2Cgkx.js.map +0 -1
- package/dist/node/bounded-output-Bgx2Cgkx.js.map +0 -1
- package/dist/node/index-CPwJEiFl.d.cts.map +0 -1
- package/dist/node/index-DQrmrFn9.d.ts.map +0 -1
package/dist/node/node.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./command-executor-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./command-executor-wBgBdWXr.cjs"),t=require("./http-executor-CeAAZotH.cjs");let n=require("node:fs"),r=require("node:path"),i=require("node:dns/promises"),a=require("node:net"),o=require("undici/index.js");function s(e){if(typeof e!=`string`)throw TypeError(`executionRoot must be a string`);if(e.trim().length===0)throw TypeError(`executionRoot must be a non-empty string`);if(!(0,r.isAbsolute)(e))throw TypeError(`executionRoot must be an absolute path`);let t;try{t=(0,n.realpathSync)(e)}catch{throw TypeError(`executionRoot must name an existing accessible directory`)}if(!(0,n.statSync)(t).isDirectory())throw TypeError(`executionRoot must name a directory`);try{(0,n.accessSync)(t,n.constants.R_OK|n.constants.X_OK)}catch{throw TypeError(`executionRoot directory must be readable and traversable`)}return t}function c(e){let t=[],i=(0,r.resolve)(e);for(;;)try{return(0,r.join)((0,n.realpathSync)(i),...t)}catch{let n=(0,r.dirname)(i);if(n===i)return(0,r.resolve)(e);t.unshift((0,r.basename)(i)),i=n}}function l(e,t){let n=c(e),i=c(t);return i===n||i.startsWith(n+r.sep)}var u=class extends Error{constructor(e){super(e),this.name=`OwnerOnlyModeError`}};function d(e=process.platform){return e===`win32`?`windows-acl`:`posix-mode`}function f(e,t){let n=(0,r.relative)(e,t);if(n.startsWith(`..`)||(0,r.isAbsolute)(n))throw new u(`withinRoot ${e} is not an ancestor of ${t}, so this call cannot own the path between them.`);let i=n.split(r.sep).filter(e=>e.length>0),a=[e],o=e;for(let e of i)o=(0,r.join)(o,e),a.push(o);return a}function p(e,t,r={}){if(d()!==`posix-mode`)return;let i=(r.readMode??(e=>(0,n.statSync)(e).mode&4095))(e);if(i&63)throw new u(`${t} ${e} is mode 0${i.toString(8)} after being set owner-only; group or other permissions survived, so its contents are readable by another account.`)}function m(e,t={}){let r=t.makeDirectory??((e,t)=>{(0,n.mkdirSync)(e,{recursive:!0,mode:t})}),i=t.setMode??((e,t)=>(0,n.chmodSync)(e,t));r(e,448);let a=t.withinRoot===void 0?[e]:f(t.withinRoot,e);for(let e of a)d()===`posix-mode`&&i(e,448),p(e,`directory`,t)}function h(e,t){m((0,r.dirname)(e));let i=`${e}.${process.pid}.tmp`;(0,n.rmSync)(i,{force:!0}),(0,n.writeFileSync)(i,t,{encoding:`utf8`,mode:384,flag:`wx`});try{p(i,`file`),(0,n.renameSync)(i,e)}catch(e){try{(0,n.unlinkSync)(i)}catch{}throw e}p(e,`file`)}function g(e){if(d()!==`posix-mode`)return;let t;try{t=(0,n.statSync)(e).mode&4095}catch{return}t&63&&((0,n.chmodSync)(e,384),p(e,`file`))}function _(e){let t=(0,a.isIP)(e);return t===4?v(e.split(`.`).map(Number)):t!==6||y(e)}function v(e){let[t=0,n=0,r=0]=e;return t===0||t===10||t===127||t===100&&n>=64&&n<=127||t===169&&n===254||t===172&&n>=16&&n<=31||t===192&&n===0&&r===0||t===192&&n===168||t===198&&(n===18||n===19)||t>=224}function y(e){let t=x(e);if(t===void 0)return!0;let[n=0,r=0,i=0,a=0,o=0,s=0,c=0,l=0]=t;return n===0&&r===0&&i===0&&a===0&&o===0&&s===65535||n===100&&r===65435&&i===0&&a===0&&o===0&&s===0?v([c>>8,c&255,l>>8,l&255]):n===0&&r===0&&i===0&&a===0&&o===0&&s===0||(n&65024)==64512||(n&65472)==65152||(n&65472)==65216||(n&65280)==65280}function b(e){let t=e.lastIndexOf(`:`),n=e.slice(t+1);if(!n.includes(`.`))return e;if(t<0||(0,a.isIP)(n)!==4)return;let[r=0,i=0,o=0,s=0]=n.split(`.`).map(Number),c=(r<<8|i).toString(16),l=(o<<8|s).toString(16);return`${e.slice(0,t+1)}${c}:${l}`}function x(e){let t=b(e.split(`%`)[0]??e);if(t===void 0)return;let n=t.split(`::`);if(n.length>2)return;let r=e=>e.length===0?[]:e.split(`:`).map(e=>Number.parseInt(e,16)),i=r(n[0]??``),a=n.length===2?r(n[1]??``):[],o=8-i.length-a.length;if(o<0||n.length===1&&o!==0)return;let s=[...i,...Array(o).fill(0),...a];return s.some(e=>Number.isNaN(e)||e<0||e>65535)?void 0:s}const S=3e4,C=5e6,w=new Set([301,302,303,307,308]),T=new Set([`localhost`,`metadata`,`metadata.google.internal`,`metadata.goog`,`instance-data`,`instance-data.ec2.internal`]),E=async e=>(await(0,i.lookup)(e,{all:!0,verbatim:!0})).map(e=>e.address);async function D(e,t={},n=E){let r=e.href;if(e.protocol!==`http:`&&e.protocol!==`https:`)return{reason:`unsupported_scheme`,url:r,message:`Only http(s) URLs may be fetched.`};if(t.allowPrivateAddresses===!0)return;let i=e.hostname.replace(/^\[|\]$/g,``).toLowerCase();if(t.allowedHosts?.includes(i))return;if(T.has(i)||i.endsWith(`.localhost`))return{reason:`metadata_host`,url:r,message:`Destination "${i}" is a loopback or cloud-metadata host and is not reachable from tools.`};let o=(0,a.isIP)(i)?[i]:await O(i,n);if(o===void 0||o.length===0)return{reason:`unresolvable`,url:r,message:`Destination "${i}" did not resolve.`};let s=o.find(_);if(s!==void 0)return{reason:`private_destination`,url:r,message:`Destination "${i}" resolves to ${s}, a private, loopback or link-local address, and is not reachable from tools.`}}async function O(e,t){try{return await t(e)}catch{return}}async function k(e,t,n){n.throwIfAborted();let r;try{return await Promise.race([t(e),new Promise((e,t)=>{r=()=>t(n.reason),n.addEventListener(`abort`,r,{once:!0}),n.aborted&&r()})])}finally{r&&n.removeEventListener(`abort`,r)}}async function A(e,t,n,r,i){let s=e.hostname.replace(/^\[|\]$/g,``).toLowerCase(),c,l=async e=>{let t=await k(e,r.lookup??E,i);if(t.length===0||t.some(e=>(0,a.isIP)(e)===0))throw Error(`Invalid DNS answer`);return c=Object.freeze([...t]),c},u=await D(e,n,l);if(i.throwIfAborted(),u)return u;if(r.fetch){let n=await r.fetch(e.href,{...t,signal:i,redirect:`manual`});return{response:n,close:async()=>{n.body&&!n.body.locked&&await n.body.cancel().catch(()=>void 0)}}}if(c??=(0,a.isIP)(s)?[s]:await O(s,l),i.throwIfAborted(),!c||c.length===0)return{reason:`unresolvable`,url:e.href,message:`Destination did not resolve to valid IP addresses.`};let d=c.map(e=>({address:e,family:(0,a.isIP)(e)})),f=new o.Agent({connections:1,pipelining:0,connect:{lookup:(e,t,n)=>{if(e.toLowerCase()!==s||i.aborted){n(Error(`Connection authority unavailable`),``,4);return}let r=typeof t==`number`?t:t.family,a=d.filter(e=>!r||e.family===r);if(a.length===0){n(Error(`No validated address family`),``,4);return}typeof t==`object`&&t.all?n(null,a):n(null,a[0].address,a[0].family)}}});if(typeof f.dispatch!=`function`||typeof f.destroy!=`function`)throw Error(`Pinned HTTP transport is unavailable in this runtime`);try{return{response:await(0,o.fetch)(e.href,{...t,signal:i,redirect:`manual`,dispatcher:f}),close:async()=>{await f.destroy()}}}catch(e){throw await f.destroy(),e}}async function j(e,t={},n={},r={}){let i=t.maxResponseBytes??C,a=n.maxRedirects??5,o=AbortSignal.timeout(t.timeoutMs??S),s=t.signal?AbortSignal.any([o,t.signal]):o,c=new URL(e),l={...t.headers??{}};for(let e=0;;e+=1){let t=await A(c,{headers:l,signal:s,redirect:`manual`},n,r,s);if(!(`response`in t))return{ok:!1,rejection:t};try{let n=t.response;if(!w.has(n.status))return await P(n,c.href,i,s);let r=n.headers.get(`location`);if(r===null)return{ok:!1,rejection:{reason:`redirect_without_location`,url:c.href,message:`HTTP ${n.status} without a Location header.`}};if(e>=a)return{ok:!1,rejection:{reason:`redirect_limit`,url:c.href,message:`More than ${a} redirects.`}};let o=new URL(r,c);o.origin!==c.origin&&(l=N(l)),c=o}finally{await t.close()}}}async function M(e,t,n={},r={}){let i=t.maxResponseBytes??C,a=AbortSignal.timeout(t.timeoutMs??S),o=t.signal?AbortSignal.any([a,t.signal]):a,s=new URL(e),c=await A(s,{method:`POST`,headers:{...t.headers??{}},body:t.body,signal:o,redirect:`manual`},n,r,o);if(!(`response`in c))return{ok:!1,rejection:c};try{let e=c.response;return e.status>=300&&e.status<400?(await e.body?.cancel().catch(()=>void 0),{ok:!1,rejection:{reason:`redirect_refused`,url:s.href,message:`HTTP ${e.status}: a POST is never redirected.`}}):await P(e,s.href,i,o)}finally{await c.close()}}function N(e){let t={};for(let[n,r]of Object.entries(e))n.toLowerCase()===`user-agent`&&(t[n]=r);return t}async function P(e,t,n,r){let i=e=>({ok:!1,rejection:{reason:`response_too_large`,url:t,message:`Response exceeds ${n} bytes (${e} seen).`}}),a=Number(e.headers.get(`content-length`));if(Number.isFinite(a)&&a>n)return await e.body?.cancel(),i(a);let o=[],s=0;if(!e.body&&typeof e.arrayBuffer==`function`){let t=new Uint8Array(await e.arrayBuffer());if(t.byteLength>n)return i(t.byteLength);o.push(t),s=t.byteLength}else if(e.body){let t=e.body.getReader();for(;;){r.throwIfAborted();let{done:e,value:a}=await t.read();if(e)break;if(s+=a.byteLength,s>n)return await t.cancel(),i(s);o.push(a)}}let c=new Uint8Array(s),l=0;for(let e of o)c.set(e,l),l+=e.byteLength;return{ok:!0,status:e.status,statusText:e.statusText,headers:e.headers,body:c,finalUrl:t}}exports.BLOCKED_HOSTNAMES=T,exports.CommandExecutor=e.t,exports.HttpExecutor=t.t,exports.OWNER_ONLY_DIRECTORY_MODE=448,exports.OWNER_ONLY_FILE_MODE=384,exports.OwnerOnlyModeError=u,exports.canonicalizePath=c,exports.ensureOwnerOnlyDirectory=m,exports.fetchWithEgressPolicy=j,exports.isPathInside=l,exports.isPrivateAddress=_,exports.openEgressExchange=A,exports.ownerOnlyGuarantee=d,exports.postWithEgressPolicy=M,exports.rejectDestination=D,exports.resolveTrustedExecutionRoot=s,exports.tightenExistingFile=g,exports.writeOwnerOnlyFile=h;
|
package/dist/node/node.d.cts
CHANGED
|
@@ -142,7 +142,7 @@ declare class CommandExecutor implements IHookTypeExecutor {
|
|
|
142
142
|
constructor(shellExecutable?: string | undefined);
|
|
143
143
|
/**
|
|
144
144
|
* `traceEnv` is the prompt's trace for this child, handed separately so it never reaches the
|
|
145
|
-
* stdin JSON. The child sees `process.env`, then
|
|
145
|
+
* stdin JSON. The child sees `process.env`, then ConversationAgent's value, then the hook's own `env`; a hook
|
|
146
146
|
* that sets its own `TRACEPARENT` wins and gets the ambient environment unchanged.
|
|
147
147
|
*/
|
|
148
148
|
execute(definition: ICommandHookDefinition, input: IHookInput, traceEnv?: ISubprocessTraceEnv): Promise<THookOutcome>;
|
|
@@ -192,6 +192,7 @@ interface IEgressFetchOptions {
|
|
|
192
192
|
readonly maxResponseBytes?: number;
|
|
193
193
|
}
|
|
194
194
|
interface IEgressDeps {
|
|
195
|
+
/** Trusted owner/test carrier: this override owns connect-time policy and socket lifetime. */
|
|
195
196
|
readonly fetch?: typeof globalThis.fetch;
|
|
196
197
|
readonly lookup?: TEgressLookup;
|
|
197
198
|
}
|
|
@@ -216,6 +217,16 @@ type TEgressFetchResult = {
|
|
|
216
217
|
declare const BLOCKED_HOSTNAMES: ReadonlySet<string>;
|
|
217
218
|
/** Validate ONE destination URL against the policy. `undefined` means it may be reached. */
|
|
218
219
|
declare function rejectDestination(url: URL, policy?: IEgressPolicy, lookup?: TEgressLookup): Promise<IEgressRejection | undefined>;
|
|
220
|
+
interface IEgressExchange {
|
|
221
|
+
readonly response: Response;
|
|
222
|
+
close(): Promise<void>;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Open one manually redirected exchange without buffering its response. A streaming owner supplies
|
|
226
|
+
* the lifetime/deadline signal and must close the exchange on completion, cancellation and failure.
|
|
227
|
+
* No ambient dispatcher, second DNS resolution, URL rewrite or transport downgrade is selected.
|
|
228
|
+
*/
|
|
229
|
+
declare function openEgressExchange(url: URL, init: RequestInit, policy: IEgressPolicy, deps: IEgressDeps, signal: AbortSignal): Promise<IEgressExchange | IEgressRejection>;
|
|
219
230
|
/**
|
|
220
231
|
* Fetch `url` under the egress policy. Policy outcomes are RETURNED (`ok: false`); transport errors
|
|
221
232
|
* (DNS failure at connect, refused connection, TLS, abort) are THROWN, as `fetch` throws them, so the
|
|
@@ -232,5 +243,5 @@ interface IEgressPostOptions extends IEgressFetchOptions {
|
|
|
232
243
|
*/
|
|
233
244
|
declare function postWithEgressPolicy(url: string, options: IEgressPostOptions, policy?: IEgressPolicy, deps?: IEgressDeps): Promise<TEgressFetchResult>;
|
|
234
245
|
//#endregion
|
|
235
|
-
export { BLOCKED_HOSTNAMES, CommandExecutor, HttpExecutor, type IEgressDeps, type IEgressFetchOptions, type IEgressPolicy, type IEgressPostOptions, type IEgressRejection, type IOwnerOnlyIo, OWNER_ONLY_DIRECTORY_MODE, OWNER_ONLY_FILE_MODE, OwnerOnlyModeError, type TEgressFetchResult, type TEgressLookup, type TEgressRejectionReason, canonicalizePath, ensureOwnerOnlyDirectory, fetchWithEgressPolicy, isPathInside, isPrivateAddress, ownerOnlyGuarantee, postWithEgressPolicy, rejectDestination, resolveTrustedExecutionRoot, tightenExistingFile, writeOwnerOnlyFile };
|
|
246
|
+
export { BLOCKED_HOSTNAMES, CommandExecutor, HttpExecutor, type IEgressDeps, type IEgressExchange, type IEgressFetchOptions, type IEgressPolicy, type IEgressPostOptions, type IEgressRejection, type IOwnerOnlyIo, OWNER_ONLY_DIRECTORY_MODE, OWNER_ONLY_FILE_MODE, OwnerOnlyModeError, type TEgressFetchResult, type TEgressLookup, type TEgressRejectionReason, canonicalizePath, ensureOwnerOnlyDirectory, fetchWithEgressPolicy, isPathInside, isPrivateAddress, openEgressExchange, ownerOnlyGuarantee, postWithEgressPolicy, rejectDestination, resolveTrustedExecutionRoot, tightenExistingFile, writeOwnerOnlyFile };
|
|
236
247
|
//# sourceMappingURL=node.d.cts.map
|
package/dist/node/node.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.cts","names":[],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/hooks/executors/command-executor.ts","../../src/hooks/executors/http-executor.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"mappings":";;;;;;;;;iBA2BgB,4BAA4B;;;;;;;;;iBAoC5B,iBAAiB;;;;;;;;iBAwBjB,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCjC9B;;cAEA;;cAMA,2BAA2B;EAC1B,YAAA;;;;;;;;iBAYE,mBACd;;;;;;;;UAYe;WACN,iBAAiB,gBAAgB;WACjC,WAAW,gBAAgB;WAC3B,YAAY;;UAGN,gCAAgC;;;;;;;;;;;;;;WActC;;;;;;;;iBAsCK,yBACd,mBACA,UAAS;;;;;;;;;iBA8BK,mBAAmB,kBAAkB;;;;;;;iBA2CrC,oBAAoB;;;cCnIvB,2BAA2B;mBAGT;WAFpB;EAEoB,YAAA;;;;;;EAO7B,QACE,YAAY,wBACZ,OAAO,YACP,WAAW,sBACV,QAAQ;;;;cC1EA,wBAAwB;WAC1B;EAEH,QAAQ,YAAY,qBAAqB,OAAO,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;iBCP7D,iBAAiB;;;
|
|
1
|
+
{"version":3,"file":"node.d.cts","names":[],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/hooks/executors/command-executor.ts","../../src/hooks/executors/http-executor.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"mappings":";;;;;;;;;iBA2BgB,4BAA4B;;;;;;;;;iBAoC5B,iBAAiB;;;;;;;;iBAwBjB,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCjC9B;;cAEA;;cAMA,2BAA2B;EAC1B,YAAA;;;;;;;;iBAYE,mBACd;;;;;;;;UAYe;WACN,iBAAiB,gBAAgB;WACjC,WAAW,gBAAgB;WAC3B,YAAY;;UAGN,gCAAgC;;;;;;;;;;;;;;WActC;;;;;;;;iBAsCK,yBACd,mBACA,UAAS;;;;;;;;;iBA8BK,mBAAmB,kBAAkB;;;;;;;iBA2CrC,oBAAoB;;;cCnIvB,2BAA2B;mBAGT;WAFpB;EAEoB,YAAA;;;;;;EAO7B,QACE,YAAY,wBACZ,OAAO,YACP,WAAW,sBACV,QAAQ;;;;cC1EA,wBAAwB;WAC1B;EAEH,QAAQ,YAAY,qBAAqB,OAAO,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;iBCP7D,iBAAiB;;;KCWrB,iBAAiB,qBAAqB;UAEjC;;WAEN;;WAEA;;WAEA;;UAGM;WACN,UAAU,SAAS;WACnB,SAAS;;WAET;;WAEA;;UAGM;;WAEN,eAAe,WAAW;WAC1B,SAAS;;KAGR;UAUK;WACN,QAAQ;WACR;WACA;;KAGC;EAEN;EACA;EACA;EACA,SAAS;EACT,MAAM;EACN;;EAEA;EAAW,WAAW;;;cAWf,mBAAmB;;iBAaV,kBACpB,KAAK,KACL,SAAQ,eACR,SAAQ,gBACP,QAAQ;UAmDM;WACN,UAAU;EACnB,SAAS;;;;;;;iBA8BW,mBACpB,KAAK,KACL,MAAM,aACN,QAAQ,eACR,MAAM,aACN,QAAQ,cACP,QAAQ,kBAAkB;;;;;;iBA6EP,sBACpB,aACA,UAAS,qBACT,SAAQ,eACR,OAAM,cACL,QAAQ;UAqDM,2BAA2B;;WAEjC,eAAe;;;;;;iBAOJ,qBACpB,aACA,SAAS,oBACT,SAAQ,eACR,OAAM,cACL,QAAQ"}
|
package/dist/node/node.d.ts
CHANGED
|
@@ -142,7 +142,7 @@ declare class CommandExecutor implements IHookTypeExecutor {
|
|
|
142
142
|
constructor(shellExecutable?: string | undefined);
|
|
143
143
|
/**
|
|
144
144
|
* `traceEnv` is the prompt's trace for this child, handed separately so it never reaches the
|
|
145
|
-
* stdin JSON. The child sees `process.env`, then
|
|
145
|
+
* stdin JSON. The child sees `process.env`, then ConversationAgent's value, then the hook's own `env`; a hook
|
|
146
146
|
* that sets its own `TRACEPARENT` wins and gets the ambient environment unchanged.
|
|
147
147
|
*/
|
|
148
148
|
execute(definition: ICommandHookDefinition, input: IHookInput, traceEnv?: ISubprocessTraceEnv): Promise<THookOutcome>;
|
|
@@ -192,6 +192,7 @@ interface IEgressFetchOptions {
|
|
|
192
192
|
readonly maxResponseBytes?: number;
|
|
193
193
|
}
|
|
194
194
|
interface IEgressDeps {
|
|
195
|
+
/** Trusted owner/test carrier: this override owns connect-time policy and socket lifetime. */
|
|
195
196
|
readonly fetch?: typeof globalThis.fetch;
|
|
196
197
|
readonly lookup?: TEgressLookup;
|
|
197
198
|
}
|
|
@@ -216,6 +217,16 @@ type TEgressFetchResult = {
|
|
|
216
217
|
declare const BLOCKED_HOSTNAMES: ReadonlySet<string>;
|
|
217
218
|
/** Validate ONE destination URL against the policy. `undefined` means it may be reached. */
|
|
218
219
|
declare function rejectDestination(url: URL, policy?: IEgressPolicy, lookup?: TEgressLookup): Promise<IEgressRejection | undefined>;
|
|
220
|
+
interface IEgressExchange {
|
|
221
|
+
readonly response: Response;
|
|
222
|
+
close(): Promise<void>;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Open one manually redirected exchange without buffering its response. A streaming owner supplies
|
|
226
|
+
* the lifetime/deadline signal and must close the exchange on completion, cancellation and failure.
|
|
227
|
+
* No ambient dispatcher, second DNS resolution, URL rewrite or transport downgrade is selected.
|
|
228
|
+
*/
|
|
229
|
+
declare function openEgressExchange(url: URL, init: RequestInit, policy: IEgressPolicy, deps: IEgressDeps, signal: AbortSignal): Promise<IEgressExchange | IEgressRejection>;
|
|
219
230
|
/**
|
|
220
231
|
* Fetch `url` under the egress policy. Policy outcomes are RETURNED (`ok: false`); transport errors
|
|
221
232
|
* (DNS failure at connect, refused connection, TLS, abort) are THROWN, as `fetch` throws them, so the
|
|
@@ -232,5 +243,5 @@ interface IEgressPostOptions extends IEgressFetchOptions {
|
|
|
232
243
|
*/
|
|
233
244
|
declare function postWithEgressPolicy(url: string, options: IEgressPostOptions, policy?: IEgressPolicy, deps?: IEgressDeps): Promise<TEgressFetchResult>;
|
|
234
245
|
//#endregion
|
|
235
|
-
export { BLOCKED_HOSTNAMES, CommandExecutor, HttpExecutor, type IEgressDeps, type IEgressFetchOptions, type IEgressPolicy, type IEgressPostOptions, type IEgressRejection, type IOwnerOnlyIo, OWNER_ONLY_DIRECTORY_MODE, OWNER_ONLY_FILE_MODE, OwnerOnlyModeError, type TEgressFetchResult, type TEgressLookup, type TEgressRejectionReason, canonicalizePath, ensureOwnerOnlyDirectory, fetchWithEgressPolicy, isPathInside, isPrivateAddress, ownerOnlyGuarantee, postWithEgressPolicy, rejectDestination, resolveTrustedExecutionRoot, tightenExistingFile, writeOwnerOnlyFile };
|
|
246
|
+
export { BLOCKED_HOSTNAMES, CommandExecutor, HttpExecutor, type IEgressDeps, type IEgressExchange, type IEgressFetchOptions, type IEgressPolicy, type IEgressPostOptions, type IEgressRejection, type IOwnerOnlyIo, OWNER_ONLY_DIRECTORY_MODE, OWNER_ONLY_FILE_MODE, OwnerOnlyModeError, type TEgressFetchResult, type TEgressLookup, type TEgressRejectionReason, canonicalizePath, ensureOwnerOnlyDirectory, fetchWithEgressPolicy, isPathInside, isPrivateAddress, openEgressExchange, ownerOnlyGuarantee, postWithEgressPolicy, rejectDestination, resolveTrustedExecutionRoot, tightenExistingFile, writeOwnerOnlyFile };
|
|
236
247
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","names":[],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/hooks/executors/command-executor.ts","../../src/hooks/executors/http-executor.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"mappings":";;;;;;;;;iBA2BgB,4BAA4B;;;;;;;;;iBAoC5B,iBAAiB;;;;;;;;iBAwBjB,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCjC9B;;cAEA;;cAMA,2BAA2B;EAC1B,YAAA;;;;;;;;iBAYE,mBACd;;;;;;;;UAYe;WACN,iBAAiB,gBAAgB;WACjC,WAAW,gBAAgB;WAC3B,YAAY;;UAGN,gCAAgC;;;;;;;;;;;;;;WActC;;;;;;;;iBAsCK,yBACd,mBACA,UAAS;;;;;;;;;iBA8BK,mBAAmB,kBAAkB;;;;;;;iBA2CrC,oBAAoB;;;cCnIvB,2BAA2B;mBAGT;WAFpB;EAEoB,YAAA;;;;;;EAO7B,QACE,YAAY,wBACZ,OAAO,YACP,WAAW,sBACV,QAAQ;;;;cC1EA,wBAAwB;WAC1B;EAEH,QAAQ,YAAY,qBAAqB,OAAO,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;iBCP7D,iBAAiB;;;
|
|
1
|
+
{"version":3,"file":"node.d.ts","names":[],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/hooks/executors/command-executor.ts","../../src/hooks/executors/http-executor.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"mappings":";;;;;;;;;iBA2BgB,4BAA4B;;;;;;;;;iBAoC5B,iBAAiB;;;;;;;;iBAwBjB,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCjC9B;;cAEA;;cAMA,2BAA2B;EAC1B,YAAA;;;;;;;;iBAYE,mBACd;;;;;;;;UAYe;WACN,iBAAiB,gBAAgB;WACjC,WAAW,gBAAgB;WAC3B,YAAY;;UAGN,gCAAgC;;;;;;;;;;;;;;WActC;;;;;;;;iBAsCK,yBACd,mBACA,UAAS;;;;;;;;;iBA8BK,mBAAmB,kBAAkB;;;;;;;iBA2CrC,oBAAoB;;;cCnIvB,2BAA2B;mBAGT;WAFpB;EAEoB,YAAA;;;;;;EAO7B,QACE,YAAY,wBACZ,OAAO,YACP,WAAW,sBACV,QAAQ;;;;cC1EA,wBAAwB;WAC1B;EAEH,QAAQ,YAAY,qBAAqB,OAAO,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;iBCP7D,iBAAiB;;;KCWrB,iBAAiB,qBAAqB;UAEjC;;WAEN;;WAEA;;WAEA;;UAGM;WACN,UAAU,SAAS;WACnB,SAAS;;WAET;;WAEA;;UAGM;;WAEN,eAAe,WAAW;WAC1B,SAAS;;KAGR;UAUK;WACN,QAAQ;WACR;WACA;;KAGC;EAEN;EACA;EACA;EACA,SAAS;EACT,MAAM;EACN;;EAEA;EAAW,WAAW;;;cAWf,mBAAmB;;iBAaV,kBACpB,KAAK,KACL,SAAQ,eACR,SAAQ,gBACP,QAAQ;UAmDM;WACN,UAAU;EACnB,SAAS;;;;;;;iBA8BW,mBACpB,KAAK,KACL,MAAM,aACN,QAAQ,eACR,MAAM,aACN,QAAQ,cACP,QAAQ,kBAAkB;;;;;;iBA6EP,sBACpB,aACA,UAAS,qBACT,SAAQ,eACR,OAAM,cACL,QAAQ;UAqDM,2BAA2B;;WAEjC,eAAe;;;;;;iBAOJ,qBACpB,aACA,SAAS,oBACT,SAAQ,eACR,OAAM,cACL,QAAQ"}
|
package/dist/node/node.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./command-executor-
|
|
1
|
+
import{t as e}from"./command-executor-D_UBZHxh.js";import{t}from"./http-executor-ADn4locH.js";import{accessSync as n,chmodSync as r,constants as i,mkdirSync as a,realpathSync as o,renameSync as s,rmSync as c,statSync as l,unlinkSync as u,writeFileSync as d}from"node:fs";import{basename as f,dirname as p,isAbsolute as m,join as h,relative as g,resolve as _,sep as v}from"node:path";import{lookup as y}from"node:dns/promises";import{isIP as b}from"node:net";import{Agent as x,fetch as S}from"undici/index.js";function C(e){if(typeof e!=`string`)throw TypeError(`executionRoot must be a string`);if(e.trim().length===0)throw TypeError(`executionRoot must be a non-empty string`);if(!m(e))throw TypeError(`executionRoot must be an absolute path`);let t;try{t=o(e)}catch{throw TypeError(`executionRoot must name an existing accessible directory`)}if(!l(t).isDirectory())throw TypeError(`executionRoot must name a directory`);try{n(t,i.R_OK|i.X_OK)}catch{throw TypeError(`executionRoot directory must be readable and traversable`)}return t}function w(e){let t=[],n=_(e);for(;;)try{return h(o(n),...t)}catch{let r=p(n);if(r===n)return _(e);t.unshift(f(n)),n=r}}function T(e,t){let n=w(e),r=w(t);return r===n||r.startsWith(n+v)}const E=384,D=448;var O=class extends Error{constructor(e){super(e),this.name=`OwnerOnlyModeError`}};function k(e=process.platform){return e===`win32`?`windows-acl`:`posix-mode`}function A(e,t){let n=g(e,t);if(n.startsWith(`..`)||m(n))throw new O(`withinRoot ${e} is not an ancestor of ${t}, so this call cannot own the path between them.`);let r=n.split(v).filter(e=>e.length>0),i=[e],a=e;for(let e of r)a=h(a,e),i.push(a);return i}function j(e,t,n={}){if(k()!==`posix-mode`)return;let r=(n.readMode??(e=>l(e).mode&4095))(e);if(r&63)throw new O(`${t} ${e} is mode 0${r.toString(8)} after being set owner-only; group or other permissions survived, so its contents are readable by another account.`)}function M(e,t={}){let n=t.makeDirectory??((e,t)=>{a(e,{recursive:!0,mode:t})}),i=t.setMode??((e,t)=>r(e,t));n(e,448);let o=t.withinRoot===void 0?[e]:A(t.withinRoot,e);for(let e of o)k()===`posix-mode`&&i(e,448),j(e,`directory`,t)}function N(e,t){M(p(e));let n=`${e}.${process.pid}.tmp`;c(n,{force:!0}),d(n,t,{encoding:`utf8`,mode:384,flag:`wx`});try{j(n,`file`),s(n,e)}catch(e){try{u(n)}catch{}throw e}j(e,`file`)}function P(e){if(k()!==`posix-mode`)return;let t;try{t=l(e).mode&4095}catch{return}t&63&&(r(e,384),j(e,`file`))}function F(e){let t=b(e);return t===4?I(e.split(`.`).map(Number)):t!==6||L(e)}function I(e){let[t=0,n=0,r=0]=e;return t===0||t===10||t===127||t===100&&n>=64&&n<=127||t===169&&n===254||t===172&&n>=16&&n<=31||t===192&&n===0&&r===0||t===192&&n===168||t===198&&(n===18||n===19)||t>=224}function L(e){let t=z(e);if(t===void 0)return!0;let[n=0,r=0,i=0,a=0,o=0,s=0,c=0,l=0]=t;return n===0&&r===0&&i===0&&a===0&&o===0&&s===65535||n===100&&r===65435&&i===0&&a===0&&o===0&&s===0?I([c>>8,c&255,l>>8,l&255]):n===0&&r===0&&i===0&&a===0&&o===0&&s===0||(n&65024)==64512||(n&65472)==65152||(n&65472)==65216||(n&65280)==65280}function R(e){let t=e.lastIndexOf(`:`),n=e.slice(t+1);if(!n.includes(`.`))return e;if(t<0||b(n)!==4)return;let[r=0,i=0,a=0,o=0]=n.split(`.`).map(Number),s=(r<<8|i).toString(16),c=(a<<8|o).toString(16);return`${e.slice(0,t+1)}${s}:${c}`}function z(e){let t=R(e.split(`%`)[0]??e);if(t===void 0)return;let n=t.split(`::`);if(n.length>2)return;let r=e=>e.length===0?[]:e.split(`:`).map(e=>Number.parseInt(e,16)),i=r(n[0]??``),a=n.length===2?r(n[1]??``):[],o=8-i.length-a.length;if(o<0||n.length===1&&o!==0)return;let s=[...i,...Array(o).fill(0),...a];return s.some(e=>Number.isNaN(e)||e<0||e>65535)?void 0:s}const B=3e4,V=5e6,H=new Set([301,302,303,307,308]),U=new Set([`localhost`,`metadata`,`metadata.google.internal`,`metadata.goog`,`instance-data`,`instance-data.ec2.internal`]),W=async e=>(await y(e,{all:!0,verbatim:!0})).map(e=>e.address);async function G(e,t={},n=W){let r=e.href;if(e.protocol!==`http:`&&e.protocol!==`https:`)return{reason:`unsupported_scheme`,url:r,message:`Only http(s) URLs may be fetched.`};if(t.allowPrivateAddresses===!0)return;let i=e.hostname.replace(/^\[|\]$/g,``).toLowerCase();if(t.allowedHosts?.includes(i))return;if(U.has(i)||i.endsWith(`.localhost`))return{reason:`metadata_host`,url:r,message:`Destination "${i}" is a loopback or cloud-metadata host and is not reachable from tools.`};let a=b(i)?[i]:await K(i,n);if(a===void 0||a.length===0)return{reason:`unresolvable`,url:r,message:`Destination "${i}" did not resolve.`};let o=a.find(F);if(o!==void 0)return{reason:`private_destination`,url:r,message:`Destination "${i}" resolves to ${o}, a private, loopback or link-local address, and is not reachable from tools.`}}async function K(e,t){try{return await t(e)}catch{return}}async function q(e,t,n){n.throwIfAborted();let r;try{return await Promise.race([t(e),new Promise((e,t)=>{r=()=>t(n.reason),n.addEventListener(`abort`,r,{once:!0}),n.aborted&&r()})])}finally{r&&n.removeEventListener(`abort`,r)}}async function J(e,t,n,r,i){let a=e.hostname.replace(/^\[|\]$/g,``).toLowerCase(),o,s=async e=>{let t=await q(e,r.lookup??W,i);if(t.length===0||t.some(e=>b(e)===0))throw Error(`Invalid DNS answer`);return o=Object.freeze([...t]),o},c=await G(e,n,s);if(i.throwIfAborted(),c)return c;if(r.fetch){let n=await r.fetch(e.href,{...t,signal:i,redirect:`manual`});return{response:n,close:async()=>{n.body&&!n.body.locked&&await n.body.cancel().catch(()=>void 0)}}}if(o??=b(a)?[a]:await K(a,s),i.throwIfAborted(),!o||o.length===0)return{reason:`unresolvable`,url:e.href,message:`Destination did not resolve to valid IP addresses.`};let l=o.map(e=>({address:e,family:b(e)})),u=new x({connections:1,pipelining:0,connect:{lookup:(e,t,n)=>{if(e.toLowerCase()!==a||i.aborted){n(Error(`Connection authority unavailable`),``,4);return}let r=typeof t==`number`?t:t.family,o=l.filter(e=>!r||e.family===r);if(o.length===0){n(Error(`No validated address family`),``,4);return}typeof t==`object`&&t.all?n(null,o):n(null,o[0].address,o[0].family)}}});if(typeof u.dispatch!=`function`||typeof u.destroy!=`function`)throw Error(`Pinned HTTP transport is unavailable in this runtime`);try{return{response:await S(e.href,{...t,signal:i,redirect:`manual`,dispatcher:u}),close:async()=>{await u.destroy()}}}catch(e){throw await u.destroy(),e}}async function Y(e,t={},n={},r={}){let i=t.maxResponseBytes??V,a=n.maxRedirects??5,o=AbortSignal.timeout(t.timeoutMs??B),s=t.signal?AbortSignal.any([o,t.signal]):o,c=new URL(e),l={...t.headers??{}};for(let e=0;;e+=1){let t=await J(c,{headers:l,signal:s,redirect:`manual`},n,r,s);if(!(`response`in t))return{ok:!1,rejection:t};try{let n=t.response;if(!H.has(n.status))return await Q(n,c.href,i,s);let r=n.headers.get(`location`);if(r===null)return{ok:!1,rejection:{reason:`redirect_without_location`,url:c.href,message:`HTTP ${n.status} without a Location header.`}};if(e>=a)return{ok:!1,rejection:{reason:`redirect_limit`,url:c.href,message:`More than ${a} redirects.`}};let o=new URL(r,c);o.origin!==c.origin&&(l=Z(l)),c=o}finally{await t.close()}}}async function X(e,t,n={},r={}){let i=t.maxResponseBytes??V,a=AbortSignal.timeout(t.timeoutMs??B),o=t.signal?AbortSignal.any([a,t.signal]):a,s=new URL(e),c=await J(s,{method:`POST`,headers:{...t.headers??{}},body:t.body,signal:o,redirect:`manual`},n,r,o);if(!(`response`in c))return{ok:!1,rejection:c};try{let e=c.response;return e.status>=300&&e.status<400?(await e.body?.cancel().catch(()=>void 0),{ok:!1,rejection:{reason:`redirect_refused`,url:s.href,message:`HTTP ${e.status}: a POST is never redirected.`}}):await Q(e,s.href,i,o)}finally{await c.close()}}function Z(e){let t={};for(let[n,r]of Object.entries(e))n.toLowerCase()===`user-agent`&&(t[n]=r);return t}async function Q(e,t,n,r){let i=e=>({ok:!1,rejection:{reason:`response_too_large`,url:t,message:`Response exceeds ${n} bytes (${e} seen).`}}),a=Number(e.headers.get(`content-length`));if(Number.isFinite(a)&&a>n)return await e.body?.cancel(),i(a);let o=[],s=0;if(!e.body&&typeof e.arrayBuffer==`function`){let t=new Uint8Array(await e.arrayBuffer());if(t.byteLength>n)return i(t.byteLength);o.push(t),s=t.byteLength}else if(e.body){let t=e.body.getReader();for(;;){r.throwIfAborted();let{done:e,value:a}=await t.read();if(e)break;if(s+=a.byteLength,s>n)return await t.cancel(),i(s);o.push(a)}}let c=new Uint8Array(s),l=0;for(let e of o)c.set(e,l),l+=e.byteLength;return{ok:!0,status:e.status,statusText:e.statusText,headers:e.headers,body:c,finalUrl:t}}export{U as BLOCKED_HOSTNAMES,e as CommandExecutor,t as HttpExecutor,D as OWNER_ONLY_DIRECTORY_MODE,E as OWNER_ONLY_FILE_MODE,O as OwnerOnlyModeError,w as canonicalizePath,M as ensureOwnerOnlyDirectory,Y as fetchWithEgressPolicy,T as isPathInside,F as isPrivateAddress,J as openEgressExchange,k as ownerOnlyGuarantee,X as postWithEgressPolicy,G as rejectDestination,C as resolveTrustedExecutionRoot,P as tightenExistingFile,N as writeOwnerOnlyFile};
|
|
2
2
|
//# sourceMappingURL=node.js.map
|
package/dist/node/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","names":["relative","relativePath","dnsLookup","resolve"],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"sourcesContent":["/**\n * SEC-006 — the SSOT for \"is this path inside that root?\" when the answer is a SECURITY decision.\n *\n * `path.resolve` and `path.normalize` are purely lexical: they collapse `.` and `..` textually but do\n * not consult the filesystem, so they cannot see a symlink. A link sitting INSIDE the root and\n * pointing outside it therefore satisfies a `startsWith(root + sep)` check while the syscall that\n * follows — `open`, `readFile`, `writeFile` — follows the link straight out of the boundary.\n *\n * Symlinks are ordinary committed git content and ordinary build-output artifacts, so this is not a\n * theoretical hole: it was exploitable in both the file-tool sandbox and the CLI's monitor asset\n * server. Both now decide containment here, because two containment checks that can disagree are\n * their own defect.\n *\n * Every security-boundary containment check in the repo routes through here — the `agent-*` family\n * and the `dag-*` family alike. The DAG side is not a cross-family exception: `dag-framework`\n * declares `@robota-sdk/agent-core` in its runtime dependencies.\n */\nimport { accessSync, constants, realpathSync, statSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';\n\n/**\n * Validate an authority root more strictly than candidate-path canonicalization.\n *\n * Candidate write paths may have a missing tail, but an authority root must already exist and be a\n * traversable directory. Keeping the contracts separate prevents a typo in a root from degrading to\n * a lexical boundary that the caller did not actually select.\n */\nexport function resolveTrustedExecutionRoot(input: unknown): string {\n if (typeof input !== 'string') {\n throw new TypeError('executionRoot must be a string');\n }\n if (input.trim().length === 0) {\n throw new TypeError('executionRoot must be a non-empty string');\n }\n if (!isAbsolute(input)) {\n throw new TypeError('executionRoot must be an absolute path');\n }\n\n let canonicalRoot: string;\n try {\n canonicalRoot = realpathSync(input);\n } catch {\n throw new TypeError('executionRoot must name an existing accessible directory');\n }\n if (!statSync(canonicalRoot).isDirectory()) {\n throw new TypeError('executionRoot must name a directory');\n }\n try {\n accessSync(canonicalRoot, constants.R_OK | constants.X_OK);\n } catch {\n throw new TypeError('executionRoot directory must be readable and traversable');\n }\n return canonicalRoot;\n}\n\n/**\n * Resolve `p` to its canonical form, following symlinks, tolerating a path that does not exist yet.\n *\n * `realpathSync` throws on a missing path, but callers legitimately ask about files they are about to\n * CREATE (a write or an edit). So we walk up to the deepest ancestor that does exist, canonicalize\n * that, and re-attach the remaining segments — the re-attached tail cannot itself be a symlink,\n * because it does not exist.\n */\nexport function canonicalizePath(p: string): string {\n const tail: string[] = [];\n let current = resolve(p);\n for (;;) {\n try {\n return join(realpathSync(current), ...tail);\n } catch {\n const parent = dirname(current);\n // allow-fallback: nothing along the chain exists or is readable — fall back to the lexical\n // path, which `resolve()` has already fully normalized and which is therefore safe to compare.\n if (parent === current) return resolve(p);\n tail.unshift(basename(current));\n current = parent;\n }\n }\n}\n\n/**\n * Whether `candidate` is `root` itself or lies beneath it, decided on the CANONICAL form of both.\n *\n * Canonicalizing the root as well as the candidate matters: a root that is itself reached through a\n * symlink (macOS `/tmp` -> `/private/tmp`, or a workspace under a symlinked home) would otherwise\n * spuriously reject every path inside it.\n */\nexport function isPathInside(root: string, candidate: string): boolean {\n const canonicalRoot = canonicalizePath(root);\n const canonicalCandidate = canonicalizePath(candidate);\n return canonicalCandidate === canonicalRoot || canonicalCandidate.startsWith(canonicalRoot + sep);\n}\n","/**\n * SEC-020 (issue #2021) — creating a host directory or file that only its owner can read.\n *\n * A host application persists session records, logs, settings and device credentials under a directory\n * of its own choosing. They carry prompts, model output, tool results and, in the settings case,\n * provider credentials. On a shared workstation or a CI host, every other local account reads them\n * unless the mode says otherwise.\n *\n * WHERE that directory is belongs to the host, not here — this module knows only what has to be true\n * of whatever path it is handed, which is what lets it be tested without a filesystem layout.\n *\n * ## Why this is one module and not a constant each caller repeats\n *\n * `0o600` and `0o700` were already spelled out in five separate files before this existed, and the\n * repository had already learned — in `guarded-directory.ts`, for a pairing rendezvous — that the\n * constants are the easy half. The load-bearing part is that **`mkdirSync(path, { mode })` does not\n * set the mode of a directory that already exists.** `recursive: true` returns successfully and\n * silently adopts whatever mode is there. A directory left at 0777 by an earlier version, by a\n * shared CI checkout, or by another local user who pre-created it, becomes ours with no signal.\n *\n * Measured under umask 022 before this module existed: a session log directory pre-created at 0777\n * stayed 0777 through `mkdirSync(dir, { recursive: true, mode: 0o700 })`, and its 0600 records were\n * then removable and replaceable by any local account.\n *\n * The same holds for files. `writeFileSync(path, data, { mode })` applies the mode only when the\n * file is CREATED; an existing 0644 record keeps 0644 forever, so a store written by an older\n * version is never repaired by a newer one.\n *\n * So: create, then set the mode unconditionally, then VERIFY — and verification is what makes the\n * first two checkable rather than hopeful. It is also the only step that catches an owner we did not\n * expect, which `mkdir` and `chmod` between them cannot.\n *\n * ## Windows\n *\n * `chmodSync` on win32 toggles the read-only attribute and cannot express \"owner only\"; NTFS ACL\n * inheritance governs instead, and a user's profile directory is not readable by other unprivileged\n * accounts by default. The POSIX assertion is therefore not made there, and `ownerOnlyGuarantee()` reports which\n * guarantee is in force so a caller — and a test — can state the platform's actual claim rather than\n * assume the POSIX one everywhere. A store directory placed inside a world-writable parent on\n * Windows is NOT protected by this module, and that is recorded rather than papered over.\n */\n\nimport {\n chmodSync,\n mkdirSync,\n renameSync,\n rmSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, isAbsolute, join, relative as relativePath, sep } from 'node:path';\n\n/** Owner read/write. Nothing for group, nothing for other. */\nexport const OWNER_ONLY_FILE_MODE = 0o600;\n/** Owner read/write/traverse. The traverse bit is what keeps another account out of the directory. */\nexport const OWNER_ONLY_DIRECTORY_MODE = 0o700;\n\n/** The bits that must be clear for the guarantee to hold: every group and other permission. */\nconst FORBIDDEN_BITS = 0o077;\n\n/** Raised when a path cannot be made owner-only. Never swallowed into a weaker mode. */\nexport class OwnerOnlyModeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OwnerOnlyModeError';\n }\n}\n\n/**\n * Which guarantee this platform can actually make.\n *\n * `posix-mode` — the mode bits are asserted after every create and every write.\n * `windows-acl` — inherited NTFS ACLs govern; the POSIX assertion is not made and not claimed.\n */\nexport function ownerOnlyGuarantee(\n platform: string = process.platform,\n): 'posix-mode' | 'windows-acl' {\n return platform === 'win32' ? 'windows-acl' : 'posix-mode';\n}\n\n/**\n * Seams for the failure paths, injected for the reason `guarded-directory.ts` states: a filesystem\n * that silently ignores `chmod` is a real condition — some network and FAT mounts do exactly that —\n * and it is the ONLY condition the verification step catches that create-then-chmod does not. Without\n * a seam that case is unreachable from a test, and a guard no test can reach is a guard nobody knows\n * is there. Measured: with these absent, deleting the verification entirely changed no test result.\n */\nexport interface IOwnerOnlyIo {\n readonly makeDirectory?: (target: string, mode: number) => void;\n readonly setMode?: (target: string, mode: number) => void;\n readonly readMode?: (target: string) => number;\n}\n\nexport interface IEnsureOwnerOnlyOptions extends IOwnerOnlyIo {\n /**\n * An ANCESTOR of `directory` that the caller also owns, tightened along with every segment between\n * them.\n *\n * Needed because `mkdirSync(path, { recursive: true, mode })` applies the mode only to the\n * directories it CREATES. A store root an older version already made sits at whatever mode it was\n * given and this call would leave it there — which is the module's own headline defect surviving\n * one level up, on the very path it exists to protect. Found in review of PR #2224.\n *\n * It is a parameter rather than a walk up to some inferred boundary because WHICH ancestors belong\n * to the caller is the caller's knowledge. This module must not guess that a parent is ours; it\n * would eventually chmod someone's home directory.\n */\n readonly withinRoot?: string;\n}\n\n/** Every directory from `root` down to `target`, outermost first. `root` must be an ancestor. */\nfunction segmentsFromRoot(root: string, target: string): string[] {\n const relative = relativePath(root, target);\n if (relative.startsWith('..') || isAbsolute(relative)) {\n throw new OwnerOnlyModeError(\n `withinRoot ${root} is not an ancestor of ${target}, so this call cannot own the path between them.`,\n );\n }\n const parts = relative.split(sep).filter((part) => part.length > 0);\n const chain: string[] = [root];\n let current = root;\n for (const part of parts) {\n current = join(current, part);\n chain.push(current);\n }\n return chain;\n}\n\nfunction assertOwnerOnly(target: string, kind: 'directory' | 'file', io: IOwnerOnlyIo = {}): void {\n if (ownerOnlyGuarantee() !== 'posix-mode') return;\n const mode = (io.readMode ?? ((path: string) => statSync(path).mode & 0o7777))(target);\n if ((mode & FORBIDDEN_BITS) !== 0) {\n throw new OwnerOnlyModeError(\n `${kind} ${target} is mode 0${mode.toString(8)} after being set owner-only; ` +\n 'group or other permissions survived, so its contents are readable by another account.',\n );\n }\n}\n\n/**\n * Ensure `directory` exists and only its owner can enter it.\n *\n * The `chmod` is unconditional because `mkdir` does not touch an existing directory's mode, and the\n * `stat` afterwards is what turns two hopeful calls into a checked one.\n */\nexport function ensureOwnerOnlyDirectory(\n directory: string,\n options: IEnsureOwnerOnlyOptions = {},\n): void {\n const makeDirectory =\n options.makeDirectory ??\n ((target: string, mode: number): void => {\n mkdirSync(target, { recursive: true, mode });\n });\n const setMode =\n options.setMode ?? ((target: string, mode: number): void => chmodSync(target, mode));\n makeDirectory(directory, OWNER_ONLY_DIRECTORY_MODE);\n // Every directory the caller declared it owns, not only the leaf. `mkdir` set the mode on what it\n // CREATED; a root an older version left at 0755 was created by nobody here and would keep it.\n const owned =\n options.withinRoot === undefined\n ? [directory]\n : segmentsFromRoot(options.withinRoot, directory);\n for (const target of owned) {\n if (ownerOnlyGuarantee() === 'posix-mode') setMode(target, OWNER_ONLY_DIRECTORY_MODE);\n assertOwnerOnly(target, 'directory', options);\n }\n}\n\n/**\n * Replace `filePath` atomically with content only its owner can read.\n *\n * The temp file carries the mode from the moment it is created — writing it at the umask's default\n * and tightening afterwards leaves a window in which the full content is world-readable on disk, and\n * `rename` preserves whatever mode the temp file had. The `chmod` after the write is for the case\n * where the temp path already existed at a wider mode.\n */\nexport function writeOwnerOnlyFile(filePath: string, content: string): void {\n ensureOwnerOnlyDirectory(dirname(filePath));\n const temporaryPath = `${filePath}.${process.pid}.tmp`;\n // A stale temp file is REMOVED rather than reused. It can only come from a process that died\n // mid-write, its contents are meaningless, and — the part that matters here — `writeFileSync` does\n // not apply `mode` to a file that already exists, so reusing one at 0666 would put the whole\n // record on disk world-readable before the rename.\n //\n // Removed with `force` rather than `if (exists) unlink`: checking and then acting on a path is a\n // race, and the race is exploitable here — another process can create the path between the two\n // calls, and the `wx` write below would then fail on a file this call believed it had cleared.\n // CodeQL flagged the check-then-act form on PR #2224 and it was right to.\n rmSync(temporaryPath, { force: true });\n // `wx` guarantees this call CREATES the file, which is the only condition under which `mode` is\n // applied. There is deliberately no `chmod` after it: tightening a file that was created wide\n // leaves a window in which the full record is readable, and a `chmod` here would make the mode\n // above redundant — so a mutation that removed it would change nothing and the window would be\n // untested. The assertion below is what proves the mode actually took.\n writeFileSync(temporaryPath, content, {\n encoding: 'utf8',\n mode: OWNER_ONLY_FILE_MODE,\n flag: 'wx',\n });\n try {\n assertOwnerOnly(temporaryPath, 'file');\n renameSync(temporaryPath, filePath);\n } catch (error) {\n try {\n unlinkSync(temporaryPath);\n } catch {\n // allow-fallback: the temp file may already be gone; the original error is the one to report.\n }\n throw error;\n }\n assertOwnerOnly(filePath, 'file');\n}\n\n/**\n * Tighten a file that already exists — the repair path for a record an older version left at 0644.\n *\n * A no-op on a path that does not exist, because \"there is nothing to tighten\" and \"we tightened it\"\n * are the same outcome for the caller, and throwing would make every writer branch on existence.\n */\nexport function tightenExistingFile(filePath: string): void {\n if (ownerOnlyGuarantee() !== 'posix-mode') return;\n let mode: number;\n try {\n mode = statSync(filePath).mode & 0o7777;\n } catch {\n return; // allow-fallback: nothing to tighten\n }\n if ((mode & FORBIDDEN_BITS) === 0) return;\n chmodSync(filePath, OWNER_ONLY_FILE_MODE);\n assertOwnerOnly(filePath, 'file');\n}\n","/**\n * Address classification for the egress boundary (#2026) — the \"is this address public unicast?\"\n * half of `egress-policy.ts`, split out so each module holds one job: that one owns the FETCH\n * (scheme, redirects, headers, deadline, byte cap), this one owns the ARITHMETIC of an address.\n *\n * The awkward part lives here on purpose. A literal destination reaches the policy already\n * normalized by `URL` (`2130706433`, `0x7f.1` and `[::ffff:7f00:1]` all collapse to one hostname),\n * but a RESOLVED one does not: `dns.lookup` renders an IPv4-mapped AAAA answer through `inet_ntop`,\n * which prints the dotted spelling `::ffff:127.0.0.1`. So the expansion below accepts BOTH\n * spellings rather than trusting any normalizer to have run — on the resolution path an attacker\n * controls the record. Every \"cannot parse\" answer is `undefined`/`true`, i.e. NOT public: an\n * address this module cannot read is never treated as reachable.\n *\n * Node-only (`node:net`): reached through `egress-policy.ts`, never the browser barrel.\n */\n\nimport { isIP } from 'node:net';\n\n/** What `isIP` answers: 4 for an IPv4 literal, 6 for an IPv6 one, 0 for text that is neither. */\nconst IP_VERSION_V4 = 4;\nconst IP_VERSION_V6 = 6;\n/** An IPv6 group is two octets wide, and is written in hex once a dotted quad is folded into one. */\nconst OCTET_BITS = 8;\nconst HEX_RADIX = 16;\n\n/** True when `ip` (v4 or v6 text) is not a public unicast address. */\nexport function isPrivateAddress(ip: string): boolean {\n const version = isIP(ip);\n if (version === IP_VERSION_V4) return isPrivateIpv4(ip.split('.').map(Number));\n if (version === IP_VERSION_V6) return isPrivateIpv6(ip);\n return true; // not an address at all: never treat it as public\n}\n\nfunction isPrivateIpv4(o: readonly number[]): boolean {\n const [a = 0, b = 0, c = 0] = o;\n return (\n a === 0 || // \"this\" network\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) || // CGNAT\n (a === 169 && b === 254) || // link-local, incl. 169.254.169.254\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 0 && c === 0) ||\n (a === 192 && b === 168) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224 // multicast + reserved + broadcast\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const groups = expandIpv6(ip);\n if (groups === undefined) return true;\n const [g0 = 0, g1 = 0, g2 = 0, g3 = 0, g4 = 0, g5 = 0, g6 = 0, g7 = 0] = groups;\n const isMapped = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0xffff;\n const isNat64 = g0 === 0x64 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;\n if (isMapped || isNat64) {\n return isPrivateIpv4([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n // ::/96 in one rule: the unspecified address `::`, loopback `::1`, and the DEPRECATED\n // IPv4-compatible form (`::127.0.0.1`, which `URL` renders as `::7f00:1`) — which embeds an IPv4\n // address just as a mapped one does. None of that range is public unicast.\n if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) return true;\n if ((g0 & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local\n if ((g0 & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((g0 & 0xffc0) === 0xfec0) return true; // fec0::/10 site-local (deprecated, still routable)\n if ((g0 & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n return false;\n}\n\n/**\n * Rewrite a trailing dotted-quad (`::ffff:127.0.0.1`) into the two hex groups it stands for, so the\n * rest of the expansion only ever sees colon groups. `undefined` when the tail is not a valid IPv4\n * address — a refusal, never a guess, because {@link isPrivateIpv6} reads `undefined` as private.\n *\n * This form is not exotic: `URL` normalizes a literal to `[::ffff:7f00:1]`, but `dns.lookup` renders\n * an IPv4-mapped AAAA answer through `inet_ntop`, which prints the dotted form — so it is exactly\n * what arrives on the RESOLUTION path, where an attacker controls the record.\n */\nfunction withoutDottedQuadTail(ip: string): string | undefined {\n const lastColon = ip.lastIndexOf(':');\n const tail = ip.slice(lastColon + 1);\n if (!tail.includes('.')) return ip;\n if (lastColon < 0 || isIP(tail) !== IP_VERSION_V4) return undefined;\n const [a = 0, b = 0, c = 0, d = 0] = tail.split('.').map(Number);\n const high = ((a << OCTET_BITS) | b).toString(HEX_RADIX);\n const low = ((c << OCTET_BITS) | d).toString(HEX_RADIX);\n return `${ip.slice(0, lastColon + 1)}${high}:${low}`;\n}\n\n/** Expand an IPv6 literal (as `URL` or a resolver renders it, brackets removed) into 8 16-bit groups. */\nfunction expandIpv6(ip: string): number[] | undefined {\n const zoneless = ip.split('%')[0] ?? ip;\n const withoutZone = withoutDottedQuadTail(zoneless);\n if (withoutZone === undefined) return undefined;\n const halves = withoutZone.split('::');\n if (halves.length > 2) return undefined;\n const parse = (part: string): number[] =>\n part.length === 0 ? [] : part.split(':').map((g) => Number.parseInt(g, 16));\n const head = parse(halves[0] ?? '');\n const tail = halves.length === 2 ? parse(halves[1] ?? '') : [];\n const missing = 8 - head.length - tail.length;\n if (missing < 0 || (halves.length === 1 && missing !== 0)) return undefined;\n const groups = [...head, ...new Array<number>(missing).fill(0), ...tail];\n return groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff) ? undefined : groups;\n}\n","/**\n * The shared outbound-network (egress) boundary (#2026).\n *\n * Every built-in, provider helper and node runtime that fetches a caller- or model-supplied URL goes\n * through {@link fetchWithEgressPolicy}. It owns, in one place:\n *\n * - DESTINATION safety: only `http:`/`https:`; literal and RESOLVED addresses are refused when they are\n * loopback, private, link-local, multicast, unspecified or reserved — for IPv4, IPv6, and IPv4-mapped /\n * IPv4-compatible / NAT64-embedded forms — plus the well-known cloud metadata hostnames. `URL`\n * normalization is what collapses alternate encodings (`2130706433`, `0x7f.1`, `[::ffff:7f00:1]`)\n * into one hostname on the LITERAL path; the RESOLVED path has no such normalizer, and an embedded\n * IPv4 arrives from `dns.lookup` in the dotted spelling `inet_ntop` prints (`::ffff:127.0.0.1`), so\n * the classifier accepts both spellings rather than trusting `URL` to have canonicalised anything.\n * - REDIRECTS: followed manually, every hop re-validated, and caller-supplied plus credential-bearing\n * headers dropped on a cross-origin hop. A POST ({@link postWithEgressPolicy}) never follows one:\n * its body can carry a credential — an authorization code, a client secret, a refresh token — and\n * a redirect would hand it to a destination the caller never chose.\n * - RESOURCE policy: the caller's `AbortSignal` composed with an explicit deadline, and the response\n * byte cap enforced WHILE STREAMING (and from `Content-Length` first), never after a whole-body\n * allocation.\n * - ESCAPE HATCHES: `allowedHosts` (exact hostnames that may resolve privately) and\n * `allowPrivateAddresses` (an explicit enterprise opt-out), both declared by the composition root.\n *\n * What it does NOT yet do: pin the connection to the validated address (Node's global `fetch` exposes\n * no connect-time lookup hook without the `undici` package). Resolve-then-validate on every hop narrows\n * the DNS-rebinding window; it does not close it, and this comment is the record of that gap.\n *\n * Node-only (`node:dns`, `node:net`): exported from `@robota-sdk/agent-core/node`, never the browser barrel.\n */\n\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIP } from 'node:net';\n\nimport { isPrivateAddress } from './ip-address.js';\n\nexport type TEgressLookup = (hostname: string) => Promise<readonly string[]>;\n\nexport interface IEgressPolicy {\n /** Exact lowercase hostnames that may be reached even when they resolve to a private address. */\n readonly allowedHosts?: readonly string[];\n /** Explicit enterprise opt-out from private/loopback/metadata blocking. Default `false`. */\n readonly allowPrivateAddresses?: boolean;\n /** Redirect hops followed before refusing. Default 5. */\n readonly maxRedirects?: number;\n}\n\nexport interface IEgressFetchOptions {\n readonly headers?: Readonly<Record<string, string>>;\n readonly signal?: AbortSignal;\n /** Deadline for the WHOLE exchange, redirects and body included. Default 30 000 ms. */\n readonly timeoutMs?: number;\n /** Byte cap enforced on `Content-Length` and again while streaming. Default 5 000 000. */\n readonly maxResponseBytes?: number;\n}\n\nexport interface IEgressDeps {\n readonly fetch?: typeof globalThis.fetch;\n readonly lookup?: TEgressLookup;\n}\n\nexport type TEgressRejectionReason =\n | 'unsupported_scheme'\n | 'private_destination'\n | 'metadata_host'\n | 'unresolvable'\n | 'redirect_limit'\n | 'redirect_without_location'\n | 'redirect_refused'\n | 'response_too_large';\n\nexport interface IEgressRejection {\n readonly reason: TEgressRejectionReason;\n readonly url: string;\n readonly message: string;\n}\n\nexport type TEgressFetchResult =\n | {\n ok: true;\n status: number;\n statusText: string;\n headers: Headers;\n body: Uint8Array;\n finalUrl: string;\n }\n | { ok: false; rejection: IEgressRejection };\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RESPONSE_BYTES = 5_000_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** The address classifier, re-exported so the egress boundary stays one import for callers. */\nexport { isPrivateAddress };\n\n/** Hostnames that are metadata services or loopback by convention, whatever they resolve to. */\nexport const BLOCKED_HOSTNAMES: ReadonlySet<string> = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\nconst defaultLookup: TEgressLookup = async (hostname) =>\n (await dnsLookup(hostname, { all: true, verbatim: true })).map((entry) => entry.address);\n\n/** Validate ONE destination URL against the policy. `undefined` means it may be reached. */\nexport async function rejectDestination(\n url: URL,\n policy: IEgressPolicy = {},\n lookup: TEgressLookup = defaultLookup,\n): Promise<IEgressRejection | undefined> {\n const href = url.href;\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n return {\n reason: 'unsupported_scheme',\n url: href,\n message: `Only http(s) URLs may be fetched.`,\n };\n }\n if (policy.allowPrivateAddresses === true) return undefined;\n const hostname = url.hostname.replace(/^\\[|\\]$/g, '').toLowerCase();\n if (policy.allowedHosts?.includes(hostname)) return undefined;\n if (BLOCKED_HOSTNAMES.has(hostname) || hostname.endsWith('.localhost')) {\n return {\n reason: 'metadata_host',\n url: href,\n message: `Destination \"${hostname}\" is a loopback or cloud-metadata host and is not reachable from tools.`,\n };\n }\n const addresses = isIP(hostname) ? [hostname] : await resolve(hostname, lookup);\n if (addresses === undefined || addresses.length === 0) {\n return {\n reason: 'unresolvable',\n url: href,\n message: `Destination \"${hostname}\" did not resolve.`,\n };\n }\n const blocked = addresses.find(isPrivateAddress);\n if (blocked !== undefined) {\n return {\n reason: 'private_destination',\n url: href,\n message: `Destination \"${hostname}\" resolves to ${blocked}, a private, loopback or link-local address, and is not reachable from tools.`,\n };\n }\n return undefined;\n}\n\nasync function resolve(\n hostname: string,\n lookup: TEgressLookup,\n): Promise<readonly string[] | undefined> {\n try {\n return await lookup(hostname);\n } catch {\n // allow-fallback: an unresolvable host is a POLICY outcome (\"unresolvable\"), reported to the\n // caller as a rejection with its reason — not a hidden success.\n return undefined;\n }\n}\n\n/**\n * Fetch `url` under the egress policy. Policy outcomes are RETURNED (`ok: false`); transport errors\n * (DNS failure at connect, refused connection, TLS, abort) are THROWN, as `fetch` throws them, so the\n * caller's own error classification keeps working.\n */\nexport async function fetchWithEgressPolicy(\n url: string,\n options: IEgressFetchOptions = {},\n policy: IEgressPolicy = {},\n deps: IEgressDeps = {},\n): Promise<TEgressFetchResult> {\n const doFetch = deps.fetch ?? ((input, init) => globalThis.fetch(input, init));\n const lookup = deps.lookup ?? defaultLookup;\n const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const maxRedirects = policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n const deadline = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n const signal = options.signal ? AbortSignal.any([deadline, options.signal]) : deadline;\n\n let current = new URL(url);\n let headers: Record<string, string> = { ...(options.headers ?? {}) };\n for (let hop = 0; ; hop += 1) {\n const rejection = await rejectDestination(current, policy, lookup);\n if (rejection !== undefined) return { ok: false, rejection };\n\n const response = await doFetch(current.href, { headers, signal, redirect: 'manual' });\n if (!REDIRECT_STATUSES.has(response.status)) {\n return readUnderCap(response, current.href, maxBytes, signal);\n }\n const location = response.headers.get('location');\n if (location === null) {\n return {\n ok: false,\n rejection: {\n reason: 'redirect_without_location',\n url: current.href,\n message: `HTTP ${response.status} without a Location header.`,\n },\n };\n }\n if (hop >= maxRedirects) {\n return {\n ok: false,\n rejection: {\n reason: 'redirect_limit',\n url: current.href,\n message: `More than ${maxRedirects} redirects.`,\n },\n };\n }\n const next = new URL(location, current);\n if (next.origin !== current.origin) headers = stripSensitiveHeaders(headers);\n current = next;\n }\n}\n\nexport interface IEgressPostOptions extends IEgressFetchOptions {\n /** Sent as-is; a `URLSearchParams` or JSON string, never a stream, so it is never re-sent. */\n readonly body: string | URLSearchParams;\n}\n\n/**\n * POST `url` under the egress policy. The destination is validated as for a GET, but a redirect is\n * REFUSED (`redirect_refused`), never followed: the body is the caller's to send to this URL alone.\n */\nexport async function postWithEgressPolicy(\n url: string,\n options: IEgressPostOptions,\n policy: IEgressPolicy = {},\n deps: IEgressDeps = {},\n): Promise<TEgressFetchResult> {\n const doFetch = deps.fetch ?? ((input, init) => globalThis.fetch(input, init));\n const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const deadline = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n const signal = options.signal ? AbortSignal.any([deadline, options.signal]) : deadline;\n const target = new URL(url);\n const rejection = await rejectDestination(target, policy, deps.lookup ?? defaultLookup);\n if (rejection !== undefined) return { ok: false, rejection };\n const response = await doFetch(target.href, {\n method: 'POST',\n headers: { ...(options.headers ?? {}) },\n body: options.body,\n signal,\n redirect: 'manual',\n });\n if (response.status >= 300 && response.status < 400) {\n await response.body?.cancel().catch(() => undefined);\n return {\n ok: false,\n rejection: {\n reason: 'redirect_refused',\n url: target.href,\n message: `HTTP ${response.status}: a POST is never redirected.`,\n },\n };\n }\n return readUnderCap(response, target.href, maxBytes, signal);\n}\n\n/** On a cross-origin hop nothing the caller supplied travels on — only the identifying User-Agent. */\nfunction stripSensitiveHeaders(headers: Record<string, string>): Record<string, string> {\n const kept: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() === 'user-agent') kept[name] = value;\n }\n return kept;\n}\n\nasync function readUnderCap(\n response: Response,\n finalUrl: string,\n maxBytes: number,\n signal: AbortSignal,\n): Promise<TEgressFetchResult> {\n const tooLarge = (seen: number): TEgressFetchResult => ({\n ok: false,\n rejection: {\n reason: 'response_too_large',\n url: finalUrl,\n message: `Response exceeds ${maxBytes} bytes (${seen} seen).`,\n },\n });\n const declared = Number(response.headers.get('content-length'));\n if (Number.isFinite(declared) && declared > maxBytes) {\n await response.body?.cancel();\n return tooLarge(declared);\n }\n const chunks: Uint8Array[] = [];\n let total = 0;\n if (!response.body && typeof response.arrayBuffer === 'function') {\n // A carrier with no readable stream (a body-less Response, or a non-streaming fetch\n // implementation) can only be read whole; the cap is then checked on what it handed over.\n const whole = new Uint8Array(await response.arrayBuffer());\n if (whole.byteLength > maxBytes) return tooLarge(whole.byteLength);\n chunks.push(whole);\n total = whole.byteLength;\n } else if (response.body) {\n const reader = response.body.getReader();\n for (;;) {\n signal.throwIfAborted();\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n return tooLarge(total);\n }\n chunks.push(value);\n }\n }\n const body = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return {\n ok: true,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n body,\n finalUrl,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node.js","names":["relative","relativePath","dnsLookup","resolve","pinnedFetch"],"sources":["../../src/utils/path-containment.ts","../../src/utils/owner-only-store.ts","../../src/utils/ip-address.ts","../../src/utils/egress-policy.ts"],"sourcesContent":["/**\n * SEC-006 — the SSOT for \"is this path inside that root?\" when the answer is a SECURITY decision.\n *\n * `path.resolve` and `path.normalize` are purely lexical: they collapse `.` and `..` textually but do\n * not consult the filesystem, so they cannot see a symlink. A link sitting INSIDE the root and\n * pointing outside it therefore satisfies a `startsWith(root + sep)` check while the syscall that\n * follows — `open`, `readFile`, `writeFile` — follows the link straight out of the boundary.\n *\n * Symlinks are ordinary committed git content and ordinary build-output artifacts, so this is not a\n * theoretical hole: it was exploitable in both the file-tool sandbox and the CLI's monitor asset\n * server. Both now decide containment here, because two containment checks that can disagree are\n * their own defect.\n *\n * Every security-boundary containment check in the repo routes through here — the `agent-*` family\n * and the `dag-*` family alike. The DAG side is not a cross-family exception: `dag-framework`\n * declares `@robota-sdk/agent-core` in its runtime dependencies.\n */\nimport { accessSync, constants, realpathSync, statSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';\n\n/**\n * Validate an authority root more strictly than candidate-path canonicalization.\n *\n * Candidate write paths may have a missing tail, but an authority root must already exist and be a\n * traversable directory. Keeping the contracts separate prevents a typo in a root from degrading to\n * a lexical boundary that the caller did not actually select.\n */\nexport function resolveTrustedExecutionRoot(input: unknown): string {\n if (typeof input !== 'string') {\n throw new TypeError('executionRoot must be a string');\n }\n if (input.trim().length === 0) {\n throw new TypeError('executionRoot must be a non-empty string');\n }\n if (!isAbsolute(input)) {\n throw new TypeError('executionRoot must be an absolute path');\n }\n\n let canonicalRoot: string;\n try {\n canonicalRoot = realpathSync(input);\n } catch {\n throw new TypeError('executionRoot must name an existing accessible directory');\n }\n if (!statSync(canonicalRoot).isDirectory()) {\n throw new TypeError('executionRoot must name a directory');\n }\n try {\n accessSync(canonicalRoot, constants.R_OK | constants.X_OK);\n } catch {\n throw new TypeError('executionRoot directory must be readable and traversable');\n }\n return canonicalRoot;\n}\n\n/**\n * Resolve `p` to its canonical form, following symlinks, tolerating a path that does not exist yet.\n *\n * `realpathSync` throws on a missing path, but callers legitimately ask about files they are about to\n * CREATE (a write or an edit). So we walk up to the deepest ancestor that does exist, canonicalize\n * that, and re-attach the remaining segments — the re-attached tail cannot itself be a symlink,\n * because it does not exist.\n */\nexport function canonicalizePath(p: string): string {\n const tail: string[] = [];\n let current = resolve(p);\n for (;;) {\n try {\n return join(realpathSync(current), ...tail);\n } catch {\n const parent = dirname(current);\n // allow-fallback: nothing along the chain exists or is readable — fall back to the lexical\n // path, which `resolve()` has already fully normalized and which is therefore safe to compare.\n if (parent === current) return resolve(p);\n tail.unshift(basename(current));\n current = parent;\n }\n }\n}\n\n/**\n * Whether `candidate` is `root` itself or lies beneath it, decided on the CANONICAL form of both.\n *\n * Canonicalizing the root as well as the candidate matters: a root that is itself reached through a\n * symlink (macOS `/tmp` -> `/private/tmp`, or a workspace under a symlinked home) would otherwise\n * spuriously reject every path inside it.\n */\nexport function isPathInside(root: string, candidate: string): boolean {\n const canonicalRoot = canonicalizePath(root);\n const canonicalCandidate = canonicalizePath(candidate);\n return canonicalCandidate === canonicalRoot || canonicalCandidate.startsWith(canonicalRoot + sep);\n}\n","/**\n * SEC-020 (issue #2021) — creating a host directory or file that only its owner can read.\n *\n * A host application persists session records, logs, settings and device credentials under a directory\n * of its own choosing. They carry prompts, model output, tool results and, in the settings case,\n * provider credentials. On a shared workstation or a CI host, every other local account reads them\n * unless the mode says otherwise.\n *\n * WHERE that directory is belongs to the host, not here — this module knows only what has to be true\n * of whatever path it is handed, which is what lets it be tested without a filesystem layout.\n *\n * ## Why this is one module and not a constant each caller repeats\n *\n * `0o600` and `0o700` were already spelled out in five separate files before this existed, and the\n * repository had already learned — in `guarded-directory.ts`, for a pairing rendezvous — that the\n * constants are the easy half. The load-bearing part is that **`mkdirSync(path, { mode })` does not\n * set the mode of a directory that already exists.** `recursive: true` returns successfully and\n * silently adopts whatever mode is there. A directory left at 0777 by an earlier version, by a\n * shared CI checkout, or by another local user who pre-created it, becomes ours with no signal.\n *\n * Measured under umask 022 before this module existed: a session log directory pre-created at 0777\n * stayed 0777 through `mkdirSync(dir, { recursive: true, mode: 0o700 })`, and its 0600 records were\n * then removable and replaceable by any local account.\n *\n * The same holds for files. `writeFileSync(path, data, { mode })` applies the mode only when the\n * file is CREATED; an existing 0644 record keeps 0644 forever, so a store written by an older\n * version is never repaired by a newer one.\n *\n * So: create, then set the mode unconditionally, then VERIFY — and verification is what makes the\n * first two checkable rather than hopeful. It is also the only step that catches an owner we did not\n * expect, which `mkdir` and `chmod` between them cannot.\n *\n * ## Windows\n *\n * `chmodSync` on win32 toggles the read-only attribute and cannot express \"owner only\"; NTFS ACL\n * inheritance governs instead, and a user's profile directory is not readable by other unprivileged\n * accounts by default. The POSIX assertion is therefore not made there, and `ownerOnlyGuarantee()` reports which\n * guarantee is in force so a caller — and a test — can state the platform's actual claim rather than\n * assume the POSIX one everywhere. A store directory placed inside a world-writable parent on\n * Windows is NOT protected by this module, and that is recorded rather than papered over.\n */\n\nimport {\n chmodSync,\n mkdirSync,\n renameSync,\n rmSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, isAbsolute, join, relative as relativePath, sep } from 'node:path';\n\n/** Owner read/write. Nothing for group, nothing for other. */\nexport const OWNER_ONLY_FILE_MODE = 0o600;\n/** Owner read/write/traverse. The traverse bit is what keeps another account out of the directory. */\nexport const OWNER_ONLY_DIRECTORY_MODE = 0o700;\n\n/** The bits that must be clear for the guarantee to hold: every group and other permission. */\nconst FORBIDDEN_BITS = 0o077;\n\n/** Raised when a path cannot be made owner-only. Never swallowed into a weaker mode. */\nexport class OwnerOnlyModeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OwnerOnlyModeError';\n }\n}\n\n/**\n * Which guarantee this platform can actually make.\n *\n * `posix-mode` — the mode bits are asserted after every create and every write.\n * `windows-acl` — inherited NTFS ACLs govern; the POSIX assertion is not made and not claimed.\n */\nexport function ownerOnlyGuarantee(\n platform: string = process.platform,\n): 'posix-mode' | 'windows-acl' {\n return platform === 'win32' ? 'windows-acl' : 'posix-mode';\n}\n\n/**\n * Seams for the failure paths, injected for the reason `guarded-directory.ts` states: a filesystem\n * that silently ignores `chmod` is a real condition — some network and FAT mounts do exactly that —\n * and it is the ONLY condition the verification step catches that create-then-chmod does not. Without\n * a seam that case is unreachable from a test, and a guard no test can reach is a guard nobody knows\n * is there. Measured: with these absent, deleting the verification entirely changed no test result.\n */\nexport interface IOwnerOnlyIo {\n readonly makeDirectory?: (target: string, mode: number) => void;\n readonly setMode?: (target: string, mode: number) => void;\n readonly readMode?: (target: string) => number;\n}\n\nexport interface IEnsureOwnerOnlyOptions extends IOwnerOnlyIo {\n /**\n * An ANCESTOR of `directory` that the caller also owns, tightened along with every segment between\n * them.\n *\n * Needed because `mkdirSync(path, { recursive: true, mode })` applies the mode only to the\n * directories it CREATES. A store root an older version already made sits at whatever mode it was\n * given and this call would leave it there — which is the module's own headline defect surviving\n * one level up, on the very path it exists to protect. Found in review of PR #2224.\n *\n * It is a parameter rather than a walk up to some inferred boundary because WHICH ancestors belong\n * to the caller is the caller's knowledge. This module must not guess that a parent is ours; it\n * would eventually chmod someone's home directory.\n */\n readonly withinRoot?: string;\n}\n\n/** Every directory from `root` down to `target`, outermost first. `root` must be an ancestor. */\nfunction segmentsFromRoot(root: string, target: string): string[] {\n const relative = relativePath(root, target);\n if (relative.startsWith('..') || isAbsolute(relative)) {\n throw new OwnerOnlyModeError(\n `withinRoot ${root} is not an ancestor of ${target}, so this call cannot own the path between them.`,\n );\n }\n const parts = relative.split(sep).filter((part) => part.length > 0);\n const chain: string[] = [root];\n let current = root;\n for (const part of parts) {\n current = join(current, part);\n chain.push(current);\n }\n return chain;\n}\n\nfunction assertOwnerOnly(target: string, kind: 'directory' | 'file', io: IOwnerOnlyIo = {}): void {\n if (ownerOnlyGuarantee() !== 'posix-mode') return;\n const mode = (io.readMode ?? ((path: string) => statSync(path).mode & 0o7777))(target);\n if ((mode & FORBIDDEN_BITS) !== 0) {\n throw new OwnerOnlyModeError(\n `${kind} ${target} is mode 0${mode.toString(8)} after being set owner-only; ` +\n 'group or other permissions survived, so its contents are readable by another account.',\n );\n }\n}\n\n/**\n * Ensure `directory` exists and only its owner can enter it.\n *\n * The `chmod` is unconditional because `mkdir` does not touch an existing directory's mode, and the\n * `stat` afterwards is what turns two hopeful calls into a checked one.\n */\nexport function ensureOwnerOnlyDirectory(\n directory: string,\n options: IEnsureOwnerOnlyOptions = {},\n): void {\n const makeDirectory =\n options.makeDirectory ??\n ((target: string, mode: number): void => {\n mkdirSync(target, { recursive: true, mode });\n });\n const setMode =\n options.setMode ?? ((target: string, mode: number): void => chmodSync(target, mode));\n makeDirectory(directory, OWNER_ONLY_DIRECTORY_MODE);\n // Every directory the caller declared it owns, not only the leaf. `mkdir` set the mode on what it\n // CREATED; a root an older version left at 0755 was created by nobody here and would keep it.\n const owned =\n options.withinRoot === undefined\n ? [directory]\n : segmentsFromRoot(options.withinRoot, directory);\n for (const target of owned) {\n if (ownerOnlyGuarantee() === 'posix-mode') setMode(target, OWNER_ONLY_DIRECTORY_MODE);\n assertOwnerOnly(target, 'directory', options);\n }\n}\n\n/**\n * Replace `filePath` atomically with content only its owner can read.\n *\n * The temp file carries the mode from the moment it is created — writing it at the umask's default\n * and tightening afterwards leaves a window in which the full content is world-readable on disk, and\n * `rename` preserves whatever mode the temp file had. The `chmod` after the write is for the case\n * where the temp path already existed at a wider mode.\n */\nexport function writeOwnerOnlyFile(filePath: string, content: string): void {\n ensureOwnerOnlyDirectory(dirname(filePath));\n const temporaryPath = `${filePath}.${process.pid}.tmp`;\n // A stale temp file is REMOVED rather than reused. It can only come from a process that died\n // mid-write, its contents are meaningless, and — the part that matters here — `writeFileSync` does\n // not apply `mode` to a file that already exists, so reusing one at 0666 would put the whole\n // record on disk world-readable before the rename.\n //\n // Removed with `force` rather than `if (exists) unlink`: checking and then acting on a path is a\n // race, and the race is exploitable here — another process can create the path between the two\n // calls, and the `wx` write below would then fail on a file this call believed it had cleared.\n // CodeQL flagged the check-then-act form on PR #2224 and it was right to.\n rmSync(temporaryPath, { force: true });\n // `wx` guarantees this call CREATES the file, which is the only condition under which `mode` is\n // applied. There is deliberately no `chmod` after it: tightening a file that was created wide\n // leaves a window in which the full record is readable, and a `chmod` here would make the mode\n // above redundant — so a mutation that removed it would change nothing and the window would be\n // untested. The assertion below is what proves the mode actually took.\n writeFileSync(temporaryPath, content, {\n encoding: 'utf8',\n mode: OWNER_ONLY_FILE_MODE,\n flag: 'wx',\n });\n try {\n assertOwnerOnly(temporaryPath, 'file');\n renameSync(temporaryPath, filePath);\n } catch (error) {\n try {\n unlinkSync(temporaryPath);\n } catch {\n // allow-fallback: the temp file may already be gone; the original error is the one to report.\n }\n throw error;\n }\n assertOwnerOnly(filePath, 'file');\n}\n\n/**\n * Tighten a file that already exists — the repair path for a record an older version left at 0644.\n *\n * A no-op on a path that does not exist, because \"there is nothing to tighten\" and \"we tightened it\"\n * are the same outcome for the caller, and throwing would make every writer branch on existence.\n */\nexport function tightenExistingFile(filePath: string): void {\n if (ownerOnlyGuarantee() !== 'posix-mode') return;\n let mode: number;\n try {\n mode = statSync(filePath).mode & 0o7777;\n } catch {\n return; // allow-fallback: nothing to tighten\n }\n if ((mode & FORBIDDEN_BITS) === 0) return;\n chmodSync(filePath, OWNER_ONLY_FILE_MODE);\n assertOwnerOnly(filePath, 'file');\n}\n","/**\n * Address classification for the egress boundary (#2026) — the \"is this address public unicast?\"\n * half of `egress-policy.ts`, split out so each module holds one job: that one owns the FETCH\n * (scheme, redirects, headers, deadline, byte cap), this one owns the ARITHMETIC of an address.\n *\n * The awkward part lives here on purpose. A literal destination reaches the policy already\n * normalized by `URL` (`2130706433`, `0x7f.1` and `[::ffff:7f00:1]` all collapse to one hostname),\n * but a RESOLVED one does not: `dns.lookup` renders an IPv4-mapped AAAA answer through `inet_ntop`,\n * which prints the dotted spelling `::ffff:127.0.0.1`. So the expansion below accepts BOTH\n * spellings rather than trusting any normalizer to have run — on the resolution path an attacker\n * controls the record. Every \"cannot parse\" answer is `undefined`/`true`, i.e. NOT public: an\n * address this module cannot read is never treated as reachable.\n *\n * Node-only (`node:net`): reached through `egress-policy.ts`, never the browser barrel.\n */\n\nimport { isIP } from 'node:net';\n\n/** What `isIP` answers: 4 for an IPv4 literal, 6 for an IPv6 one, 0 for text that is neither. */\nconst IP_VERSION_V4 = 4;\nconst IP_VERSION_V6 = 6;\n/** An IPv6 group is two octets wide, and is written in hex once a dotted quad is folded into one. */\nconst OCTET_BITS = 8;\nconst HEX_RADIX = 16;\n\n/** True when `ip` (v4 or v6 text) is not a public unicast address. */\nexport function isPrivateAddress(ip: string): boolean {\n const version = isIP(ip);\n if (version === IP_VERSION_V4) return isPrivateIpv4(ip.split('.').map(Number));\n if (version === IP_VERSION_V6) return isPrivateIpv6(ip);\n return true; // not an address at all: never treat it as public\n}\n\nfunction isPrivateIpv4(o: readonly number[]): boolean {\n const [a = 0, b = 0, c = 0] = o;\n return (\n a === 0 || // \"this\" network\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) || // CGNAT\n (a === 169 && b === 254) || // link-local, incl. 169.254.169.254\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 0 && c === 0) ||\n (a === 192 && b === 168) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224 // multicast + reserved + broadcast\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const groups = expandIpv6(ip);\n if (groups === undefined) return true;\n const [g0 = 0, g1 = 0, g2 = 0, g3 = 0, g4 = 0, g5 = 0, g6 = 0, g7 = 0] = groups;\n const isMapped = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0xffff;\n const isNat64 = g0 === 0x64 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0;\n if (isMapped || isNat64) {\n return isPrivateIpv4([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n // ::/96 in one rule: the unspecified address `::`, loopback `::1`, and the DEPRECATED\n // IPv4-compatible form (`::127.0.0.1`, which `URL` renders as `::7f00:1`) — which embeds an IPv4\n // address just as a mapped one does. None of that range is public unicast.\n if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) return true;\n if ((g0 & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local\n if ((g0 & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((g0 & 0xffc0) === 0xfec0) return true; // fec0::/10 site-local (deprecated, still routable)\n if ((g0 & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n return false;\n}\n\n/**\n * Rewrite a trailing dotted-quad (`::ffff:127.0.0.1`) into the two hex groups it stands for, so the\n * rest of the expansion only ever sees colon groups. `undefined` when the tail is not a valid IPv4\n * address — a refusal, never a guess, because {@link isPrivateIpv6} reads `undefined` as private.\n *\n * This form is not exotic: `URL` normalizes a literal to `[::ffff:7f00:1]`, but `dns.lookup` renders\n * an IPv4-mapped AAAA answer through `inet_ntop`, which prints the dotted form — so it is exactly\n * what arrives on the RESOLUTION path, where an attacker controls the record.\n */\nfunction withoutDottedQuadTail(ip: string): string | undefined {\n const lastColon = ip.lastIndexOf(':');\n const tail = ip.slice(lastColon + 1);\n if (!tail.includes('.')) return ip;\n if (lastColon < 0 || isIP(tail) !== IP_VERSION_V4) return undefined;\n const [a = 0, b = 0, c = 0, d = 0] = tail.split('.').map(Number);\n const high = ((a << OCTET_BITS) | b).toString(HEX_RADIX);\n const low = ((c << OCTET_BITS) | d).toString(HEX_RADIX);\n return `${ip.slice(0, lastColon + 1)}${high}:${low}`;\n}\n\n/** Expand an IPv6 literal (as `URL` or a resolver renders it, brackets removed) into 8 16-bit groups. */\nfunction expandIpv6(ip: string): number[] | undefined {\n const zoneless = ip.split('%')[0] ?? ip;\n const withoutZone = withoutDottedQuadTail(zoneless);\n if (withoutZone === undefined) return undefined;\n const halves = withoutZone.split('::');\n if (halves.length > 2) return undefined;\n const parse = (part: string): number[] =>\n part.length === 0 ? [] : part.split(':').map((g) => Number.parseInt(g, 16));\n const head = parse(halves[0] ?? '');\n const tail = halves.length === 2 ? parse(halves[1] ?? '') : [];\n const missing = 8 - head.length - tail.length;\n if (missing < 0 || (halves.length === 1 && missing !== 0)) return undefined;\n const groups = [...head, ...new Array<number>(missing).fill(0), ...tail];\n return groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff) ? undefined : groups;\n}\n","/**\n * The shared outbound-network (egress) boundary (#2026).\n *\n * Every built-in, provider helper and node runtime that fetches a caller- or model-supplied URL goes\n * through {@link fetchWithEgressPolicy}. It owns, in one place:\n *\n * - DESTINATION safety: only `http:`/`https:`; literal and RESOLVED addresses are refused when they are\n * loopback, private, link-local, multicast, unspecified or reserved — for IPv4, IPv6, and IPv4-mapped /\n * IPv4-compatible / NAT64-embedded forms — plus the well-known cloud metadata hostnames. `URL`\n * normalization is what collapses alternate encodings (`2130706433`, `0x7f.1`, `[::ffff:7f00:1]`)\n * into one hostname on the LITERAL path; the RESOLVED path has no such normalizer, and an embedded\n * IPv4 arrives from `dns.lookup` in the dotted spelling `inet_ntop` prints (`::ffff:127.0.0.1`), so\n * the classifier accepts both spellings rather than trusting `URL` to have canonicalised anything.\n * - REDIRECTS: followed manually, every hop re-validated, and caller-supplied plus credential-bearing\n * headers dropped on a cross-origin hop. A POST ({@link postWithEgressPolicy}) never follows one:\n * its body can carry a credential — an authorization code, a client secret, a refresh token — and\n * a redirect would hand it to a destination the caller never chose.\n * - RESOURCE policy: the caller's `AbortSignal` composed with an explicit deadline, and the response\n * byte cap enforced WHILE STREAMING (and from `Content-Length` first), never after a whole-body\n * allocation.\n * - ESCAPE HATCHES: `allowedHosts` (exact hostnames that may resolve privately) and\n * `allowPrivateAddresses` (an explicit enterprise opt-out), both declared by the composition root.\n *\n * The default Node transport connects only to the validated address set through a request-owned\n * dispatcher. The original URL/Host/TLS name is preserved. A trusted injected fetch owns its own\n * socket policy; it is never an automatic fallback when the default pinned transport fails.\n *\n * Node-only (`node:dns`, `node:net`): exported from `@robota-sdk/agent-core/node`, never the browser barrel.\n */\n\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIP } from 'node:net';\n// Bun replaces the bare specifier with a shim that ignores dispatcher; this subpath loads the pinned package.\nimport { Agent, fetch as pinnedFetch } from 'undici/index.js';\n\nimport { isPrivateAddress } from './ip-address.js';\n\nexport type TEgressLookup = (hostname: string) => Promise<readonly string[]>;\n\nexport interface IEgressPolicy {\n /** Exact lowercase hostnames that may be reached even when they resolve to a private address. */\n readonly allowedHosts?: readonly string[];\n /** Explicit enterprise opt-out from private/loopback/metadata blocking. Default `false`. */\n readonly allowPrivateAddresses?: boolean;\n /** Redirect hops followed before refusing. Default 5. */\n readonly maxRedirects?: number;\n}\n\nexport interface IEgressFetchOptions {\n readonly headers?: Readonly<Record<string, string>>;\n readonly signal?: AbortSignal;\n /** Deadline for the WHOLE exchange, redirects and body included. Default 30 000 ms. */\n readonly timeoutMs?: number;\n /** Byte cap enforced on `Content-Length` and again while streaming. Default 5 000 000. */\n readonly maxResponseBytes?: number;\n}\n\nexport interface IEgressDeps {\n /** Trusted owner/test carrier: this override owns connect-time policy and socket lifetime. */\n readonly fetch?: typeof globalThis.fetch;\n readonly lookup?: TEgressLookup;\n}\n\nexport type TEgressRejectionReason =\n | 'unsupported_scheme'\n | 'private_destination'\n | 'metadata_host'\n | 'unresolvable'\n | 'redirect_limit'\n | 'redirect_without_location'\n | 'redirect_refused'\n | 'response_too_large';\n\nexport interface IEgressRejection {\n readonly reason: TEgressRejectionReason;\n readonly url: string;\n readonly message: string;\n}\n\nexport type TEgressFetchResult =\n | {\n ok: true;\n status: number;\n statusText: string;\n headers: Headers;\n body: Uint8Array;\n finalUrl: string;\n }\n | { ok: false; rejection: IEgressRejection };\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RESPONSE_BYTES = 5_000_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** The address classifier, re-exported so the egress boundary stays one import for callers. */\nexport { isPrivateAddress };\n\n/** Hostnames that are metadata services or loopback by convention, whatever they resolve to. */\nexport const BLOCKED_HOSTNAMES: ReadonlySet<string> = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\nconst defaultLookup: TEgressLookup = async (hostname) =>\n (await dnsLookup(hostname, { all: true, verbatim: true })).map((entry) => entry.address);\n\n/** Validate ONE destination URL against the policy. `undefined` means it may be reached. */\nexport async function rejectDestination(\n url: URL,\n policy: IEgressPolicy = {},\n lookup: TEgressLookup = defaultLookup,\n): Promise<IEgressRejection | undefined> {\n const href = url.href;\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n return {\n reason: 'unsupported_scheme',\n url: href,\n message: `Only http(s) URLs may be fetched.`,\n };\n }\n if (policy.allowPrivateAddresses === true) return undefined;\n const hostname = url.hostname.replace(/^\\[|\\]$/g, '').toLowerCase();\n if (policy.allowedHosts?.includes(hostname)) return undefined;\n if (BLOCKED_HOSTNAMES.has(hostname) || hostname.endsWith('.localhost')) {\n return {\n reason: 'metadata_host',\n url: href,\n message: `Destination \"${hostname}\" is a loopback or cloud-metadata host and is not reachable from tools.`,\n };\n }\n const addresses = isIP(hostname) ? [hostname] : await resolve(hostname, lookup);\n if (addresses === undefined || addresses.length === 0) {\n return {\n reason: 'unresolvable',\n url: href,\n message: `Destination \"${hostname}\" did not resolve.`,\n };\n }\n const blocked = addresses.find(isPrivateAddress);\n if (blocked !== undefined) {\n return {\n reason: 'private_destination',\n url: href,\n message: `Destination \"${hostname}\" resolves to ${blocked}, a private, loopback or link-local address, and is not reachable from tools.`,\n };\n }\n return undefined;\n}\n\nasync function resolve(\n hostname: string,\n lookup: TEgressLookup,\n): Promise<readonly string[] | undefined> {\n try {\n return await lookup(hostname);\n } catch {\n // allow-fallback: an unresolvable host is a POLICY outcome (\"unresolvable\"), reported to the\n // caller as a rejection with its reason — not a hidden success.\n return undefined;\n }\n}\n\nexport interface IEgressExchange {\n readonly response: Response;\n close(): Promise<void>;\n}\n\n/** The bounded resolver can finish later, but an aborted exchange never dispatches its result. */\nasync function lookupUnderSignal(\n hostname: string,\n lookup: TEgressLookup,\n signal: AbortSignal,\n): Promise<readonly string[]> {\n signal.throwIfAborted();\n let aborted: (() => void) | undefined;\n try {\n return await Promise.race([\n lookup(hostname),\n new Promise<never>((_, reject) => {\n aborted = () => reject(signal.reason);\n signal.addEventListener('abort', aborted, { once: true });\n if (signal.aborted) aborted();\n }),\n ]);\n } finally {\n if (aborted) signal.removeEventListener('abort', aborted);\n }\n}\n\n/**\n * Open one manually redirected exchange without buffering its response. A streaming owner supplies\n * the lifetime/deadline signal and must close the exchange on completion, cancellation and failure.\n * No ambient dispatcher, second DNS resolution, URL rewrite or transport downgrade is selected.\n */\nexport async function openEgressExchange(\n url: URL,\n init: RequestInit,\n policy: IEgressPolicy,\n deps: IEgressDeps,\n signal: AbortSignal,\n): Promise<IEgressExchange | IEgressRejection> {\n const hostname = url.hostname.replace(/^\\[|\\]$/g, '').toLowerCase();\n let addresses: readonly string[] | undefined;\n const lookup: TEgressLookup = async (host) => {\n const resolved = await lookupUnderSignal(host, deps.lookup ?? defaultLookup, signal);\n if (resolved.length === 0 || resolved.some((address) => isIP(address) === 0))\n throw new Error('Invalid DNS answer');\n addresses = Object.freeze([...resolved]);\n return addresses;\n };\n const rejection = await rejectDestination(url, policy, lookup);\n signal.throwIfAborted();\n if (rejection) return rejection;\n if (deps.fetch) {\n const response = await deps.fetch(url.href, { ...init, signal, redirect: 'manual' });\n return {\n response,\n close: async () => {\n if (response.body && !response.body.locked)\n await response.body.cancel().catch(() => undefined);\n },\n };\n }\n // Explicit private/host allowlisting skips destination blocking, never the connection's one resolution.\n addresses ??= isIP(hostname) ? [hostname] : await resolve(hostname, lookup);\n signal.throwIfAborted();\n if (!addresses || addresses.length === 0)\n return {\n reason: 'unresolvable',\n url: url.href,\n message: 'Destination did not resolve to valid IP addresses.',\n };\n const pinned = addresses.map((address) => ({ address, family: isIP(address) }));\n const dispatcher = new Agent({\n connections: 1,\n pipelining: 0,\n connect: {\n lookup: (host, options, callback) => {\n if (host.toLowerCase() !== hostname || signal.aborted) {\n callback(new Error('Connection authority unavailable'), '', 4);\n return;\n }\n const family = typeof options === 'number' ? options : options.family;\n const candidates = pinned.filter((record) => !family || record.family === family);\n if (candidates.length === 0) {\n callback(new Error('No validated address family'), '', 4);\n return;\n }\n if (typeof options === 'object' && options.all) callback(null, candidates);\n else callback(null, candidates[0]!.address, candidates[0]!.family);\n },\n },\n });\n if (typeof dispatcher.dispatch !== 'function' || typeof dispatcher.destroy !== 'function')\n throw new Error('Pinned HTTP transport is unavailable in this runtime');\n try {\n const response = await pinnedFetch(url.href, { ...init, signal, redirect: 'manual', dispatcher } as Parameters<\n typeof pinnedFetch\n >[1]);\n // undici implements the standard Response/Headers/body contract used below; DOM/Node type names differ.\n return {\n response: response as unknown as Response,\n close: async () => {\n await dispatcher.destroy();\n },\n };\n } catch (error) {\n await dispatcher.destroy();\n throw error;\n }\n}\n\n/**\n * Fetch `url` under the egress policy. Policy outcomes are RETURNED (`ok: false`); transport errors\n * (DNS failure at connect, refused connection, TLS, abort) are THROWN, as `fetch` throws them, so the\n * caller's own error classification keeps working.\n */\nexport async function fetchWithEgressPolicy(\n url: string,\n options: IEgressFetchOptions = {},\n policy: IEgressPolicy = {},\n deps: IEgressDeps = {},\n): Promise<TEgressFetchResult> {\n const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const maxRedirects = policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n const deadline = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n const signal = options.signal ? AbortSignal.any([deadline, options.signal]) : deadline;\n\n let current = new URL(url);\n let headers: Record<string, string> = { ...(options.headers ?? {}) };\n for (let hop = 0; ; hop += 1) {\n const exchange = await openEgressExchange(\n current,\n { headers, signal, redirect: 'manual' },\n policy,\n deps,\n signal,\n );\n if (!('response' in exchange)) return { ok: false, rejection: exchange };\n try {\n const response = exchange.response;\n if (!REDIRECT_STATUSES.has(response.status)) {\n return await readUnderCap(response, current.href, maxBytes, signal);\n }\n\n const location = response.headers.get('location');\n if (location === null) {\n return {\n ok: false,\n rejection: {\n reason: 'redirect_without_location',\n url: current.href,\n message: `HTTP ${response.status} without a Location header.`,\n },\n };\n }\n if (hop >= maxRedirects) {\n return {\n ok: false,\n rejection: {\n reason: 'redirect_limit',\n url: current.href,\n message: `More than ${maxRedirects} redirects.`,\n },\n };\n }\n const next = new URL(location, current);\n if (next.origin !== current.origin) headers = stripSensitiveHeaders(headers);\n current = next;\n } finally {\n await exchange.close();\n }\n }\n}\n\nexport interface IEgressPostOptions extends IEgressFetchOptions {\n /** Sent as-is; a `URLSearchParams` or JSON string, never a stream, so it is never re-sent. */\n readonly body: string | URLSearchParams;\n}\n\n/**\n * POST `url` under the egress policy. The destination is validated as for a GET, but a redirect is\n * REFUSED (`redirect_refused`), never followed: the body is the caller's to send to this URL alone.\n */\nexport async function postWithEgressPolicy(\n url: string,\n options: IEgressPostOptions,\n policy: IEgressPolicy = {},\n deps: IEgressDeps = {},\n): Promise<TEgressFetchResult> {\n const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const deadline = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n const signal = options.signal ? AbortSignal.any([deadline, options.signal]) : deadline;\n const target = new URL(url);\n const exchange = await openEgressExchange(\n target,\n {\n method: 'POST',\n headers: { ...(options.headers ?? {}) },\n body: options.body,\n signal,\n redirect: 'manual',\n },\n policy,\n deps,\n signal,\n );\n if (!('response' in exchange)) return { ok: false, rejection: exchange };\n try {\n const response = exchange.response;\n if (response.status >= 300 && response.status < 400) {\n await response.body?.cancel().catch(() => undefined);\n return {\n ok: false,\n rejection: {\n reason: 'redirect_refused',\n url: target.href,\n message: `HTTP ${response.status}: a POST is never redirected.`,\n },\n };\n }\n return await readUnderCap(response, target.href, maxBytes, signal);\n } finally {\n await exchange.close();\n }\n}\n\n/** On a cross-origin hop nothing the caller supplied travels on — only the identifying User-Agent. */\nfunction stripSensitiveHeaders(headers: Record<string, string>): Record<string, string> {\n const kept: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() === 'user-agent') kept[name] = value;\n }\n return kept;\n}\n\nasync function readUnderCap(\n response: Response,\n finalUrl: string,\n maxBytes: number,\n signal: AbortSignal,\n): Promise<TEgressFetchResult> {\n const tooLarge = (seen: number): TEgressFetchResult => ({\n ok: false,\n rejection: {\n reason: 'response_too_large',\n url: finalUrl,\n message: `Response exceeds ${maxBytes} bytes (${seen} seen).`,\n },\n });\n const declared = Number(response.headers.get('content-length'));\n if (Number.isFinite(declared) && declared > maxBytes) {\n await response.body?.cancel();\n return tooLarge(declared);\n }\n const chunks: Uint8Array[] = [];\n let total = 0;\n if (!response.body && typeof response.arrayBuffer === 'function') {\n // A carrier with no readable stream (a body-less Response, or a non-streaming fetch\n // implementation) can only be read whole; the cap is then checked on what it handed over.\n const whole = new Uint8Array(await response.arrayBuffer());\n if (whole.byteLength > maxBytes) return tooLarge(whole.byteLength);\n chunks.push(whole);\n total = whole.byteLength;\n } else if (response.body) {\n const reader = response.body.getReader();\n for (;;) {\n signal.throwIfAborted();\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n return tooLarge(total);\n }\n chunks.push(value);\n }\n }\n const body = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return {\n ok: true,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n body,\n finalUrl,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Nc as IAIProvider, Pc as IChatOptions, Zc as IToolSchema, iu as TUniversalMessage } from "../index-CagopdzH.cjs";
|
|
2
2
|
//#region src/testing/scripted-provider.d.ts
|
|
3
3
|
/** ANALYTICS-001: optional token usage a scripted turn reports, so usage-based tests are possible. */
|
|
4
4
|
interface IScriptedTurnUsage {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Nc as IAIProvider, Pc as IChatOptions, Zc as IToolSchema, iu as TUniversalMessage } from "../index-CjyNzgQN.js";
|
|
2
2
|
//#region src/testing/scripted-provider.d.ts
|
|
3
3
|
/** ANALYTICS-001: optional token usage a scripted turn reports, so usage-based tests are possible. */
|
|
4
4
|
interface IScriptedTurnUsage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=class extends Error{context;constructor(e,t){super(e),this.context=t,this.name=this.constructor.name,Object.setPrototypeOf(this,new.target.prototype)}},t=class extends e{code=`CONFIGURATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t){super(`Configuration Error: ${e}`,t)}},n=class extends e{field;code=`VALIDATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t,n){super(`Validation Error: ${e}`,n),this.field=t}},r=class extends e{issues;attempts;code=`STRUCTURED_OUTPUT_ERROR`;category=`provider`;recoverable=!0;constructor(e,t,n,r){super(`Structured Output Error: ${e}`,r),this.issues=t,this.attempts=n}},i=class extends e{provider;originalError;code=`PROVIDER_ERROR`;category=`provider`;recoverable=!0;status;type;constructor(e,t,n,r,i){super(`Provider Error (${t}): ${e}`,r),this.provider=t,this.originalError=n,i?.status!==void 0&&(this.status=i.status),i?.type!==void 0&&(this.type=i.type)}},a=class extends e{provider;code=`AUTHENTICATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t,n){super(`Authentication Error: ${e}`,n),this.provider=t}},o=class extends e{retryAfter;provider;code=`RATE_LIMIT_ERROR`;category=`provider`;recoverable=!0;constructor(e,t,n,r){super(`Rate Limit Error: ${e}`,r),this.retryAfter=t,this.provider=n}},s=class extends e{originalError;provider;code=`NETWORK_ERROR`;category=`system`;recoverable=!0;constructor(e,t,n,r){super(`Network Error: ${e}`,n),this.originalError=t,this.provider=r}},c=class extends e{toolName;originalError;code=`TOOL_EXECUTION_ERROR`;category=`system`;recoverable=!1;constructor(e,t,n,r){super(`Tool Execution Error (${t}): ${e}`,r),this.toolName=t,this.originalError=n}},l=class extends e{toolName;callCount;maxSameToolInputs;code=`SAME_TOOL_INPUT_LOOP`;category=`system`;recoverable=!0;constructor(e,t,n,r){super(`Tool "${e}" was called with identical input ${t} times, past the maxSameToolInputs limit of ${n} — stopping the turn to break the loop`,r),this.toolName=e,this.callCount=t,this.maxSameToolInputs=n}},u=class extends e{round;code=`EMPTY_COMPLETION`;category=`provider`;recoverable=!0;constructor(e,t){super(`Round ${e} ended with no tool call and no text response, and allowToolOnlyCompletion prevented the follow-up call that would otherwise recover it`,t),this.round=e}},d=class extends e{model;provider;availableModels;code=`MODEL_NOT_AVAILABLE`;category=`user`;recoverable=!1;constructor(e,t,n,r){let i=e===void 0?`No model available for provider "${t}"`:`Model "${e}" is not available for provider "${t}"`,a=r?.originalMessage;super(typeof a==`string`&&a.length>0?`${i}: ${a}`:i,r),this.model=e,this.provider=t,this.availableModels=n}},f=class extends e{code=`CIRCUIT_BREAKER_OPEN`;category=`system`;recoverable=!0;constructor(e=`Circuit breaker is open`,t){super(e,t)}},p=class extends e{pluginName;code=`PLUGIN_ERROR`;category=`system`;recoverable=!1;constructor(e,t,n){super(`Plugin Error (${t}): ${e}`,n),this.pluginName=t}},m=class extends e{code=`STORAGE_ERROR`;category=`system`;recoverable=!0;constructor(e,t){super(`Storage Error: ${e}`,t)}},h=class extends e{code=`CACHE_INTEGRITY_ERROR`;category=`system`;recoverable=!1;constructor(e,t){super(`Cache Integrity Error: ${e}`,t)}},g=class extends e{executable;code=`UNSUPPORTED_SHELL`;category=`user`;recoverable=!1;constructor(e){super(`Unsupported shell executable: ${e}`,{executable:e}),this.executable=e}};function _(e){return e===`darwin`?'Host OS: macOS (POSIX, BSD userland). Use BSD-flavored utilities: `sed -i ""` needs an empty backup arg, `date`/`grep`/`stat` flags differ from GNU. Prefer portable POSIX flags; avoid Linux/GNU-only options.':e===`linux`?"Host OS: Linux (POSIX, usually GNU coreutils, but distro/user setup varies). Prefer portable POSIX flags; do not assume a specific distro, and probe with `command -v` before relying on a non-standard tool.":`Host OS: ${e}; active shell family: POSIX sh/bash. Write portable POSIX syntax and avoid host-specific flags.`}function v(e,t){let n=x(e)===`bash`?`bash`:`sh`;return{command:e,kind:n,platform:t,commandArgs:e=>[`-c`,e],interactiveArgs:[],label:`${n} on ${t}`,syntaxHint:_(t)}}function y(e,t){return{command:e,kind:`powershell`,platform:t,commandArgs:e=>[`-NoProfile`,`-Command`,e],interactiveArgs:[`-NoProfile`],label:`PowerShell on ${t}`,syntaxHint:`Host OS: ${t}; active shell family: PowerShell. Write PowerShell syntax (not bash): e.g. \`Get-ChildItem\` not \`ls -la\`, \`\$env:VAR\` not \`\$VAR\`.`}}function b(e,t){return{command:e,kind:`cmd`,platform:t,commandArgs:e=>[`/d`,`/s`,`/c`,e],interactiveArgs:[],label:`cmd.exe on ${t}`,syntaxHint:`Host OS: ${t}; active shell family: Windows cmd.exe. Write cmd.exe syntax.`}}function x(e){return(e.split(/[\\/]/).at(-1)?.toLowerCase()??``).replace(/\.exe$/i,``)}function S(e,t){switch(x(e)){case`sh`:case`bash`:return v(e,t);case`powershell`:case`pwsh`:return y(e,t);case`cmd`:return b(e,t);default:throw new g(e)}}function C(e){let t=e?.trim();return t===void 0||t.length===0?void 0:t}function w(e={}){let t=e.env??process.env,n=e.platform??process.platform,r=C(e.executable);return r===void 0?n===`win32`?y(`powershell.exe`,n):v(C(t.SHELL)??`/bin/sh`,n):S(r,n)}const T=/^(?!0{32}$)[0-9a-f]{32}$/,E=/^(?!0{16}$)[0-9a-f]{16}$/;function D(e){return e.replaceAll(`-`,``).slice(0,16)}const O=D;function k(e){return E.test(e)}function A(e,t){if(!(!T.test(e)||!E.test(t)))return`00-${e}-${t}-01`}function j(e,t){if(e.allowedOrigins.length===0)return;let n=A(e.traceId,D(t));if(n!==void 0)return{traceparent:n,allowedOrigins:[...e.allowedOrigins]}}function M(e,t){return j(e,t)}function N(e,t,n){if(t?.subprocessClasses?.includes(e)!==!0)return;let r=A(t.traceId,n);return r===void 0?void 0:{TRACEPARENT:r}}function P(e,t,n={}){if(t===void 0||n.TRACEPARENT!==void 0)return{...e,...n};let{TRACESTATE:r,...i}=e;return{...i,...t,...n}}function F(e,t){if(e===void 0||t===void 0)return{};let n;try{n=new URL(e).origin}catch{return{}}return n===`null`||!t.allowedOrigins.includes(n)?{}:{traceparent:t.traceparent}}const I=e=>`\n…[output truncated: ${e} byte(s) dropped]`;function L(e){let t=e.retain??`head`,n=e.truncationMarker??I,r=Math.max(0,Math.floor(e.maxBytes)),i=new TextEncoder,a=[],o=0,s=0,c=e=>{let t=r-o;if(t<=0)return;let n=e.byteLength<=t?e:e.subarray(0,t);a.push(n),o+=n.byteLength},l=e=>{let t=e.byteLength<=r?e:e.subarray(e.byteLength-r);for(a.push(t),o+=t.byteLength;o>r&&a.length>0;){let e=a[0],t=o-r;e.byteLength<=t?(a.shift(),o-=e.byteLength):(a[0]=e.subarray(t),o-=t)}};return{append(e){let n=typeof e==`string`?i.encode(e):e;s+=n.byteLength,t===`head`?c(n):l(n)},get retainedBytes(){return o},get totalBytes(){return s},get droppedBytes(){return s-o},get truncated(){return s>o},toString(){let e=new Uint8Array(o),r=0;for(let t of a)e.set(t,r),r+=t.byteLength;a=a.length>1?[e]:a;let i=new TextDecoder().decode(e);if(s<=o)return i;let c=n(s-o);return t===`head`?`${i}${c}`:`${c.replace(/^\n/,``)}\n${i}`}}}const R={deny:3,ask:2,defer:1,allow:0};function z(e){return typeof e==`string`&&e in R}function B(e){if(!(typeof e!=`object`||!e)&&`additionalContext`in e)return String(e.additionalContext)}function V(e,t){let n=H(e);if(n===null){let t=e.trim();return{context:t?[t]:[]}}if(n.continue===!1)return{context:[],blockReason:typeof n.stopReason==`string`?n.stopReason:`Blocked by hook (continue: false)`};let r=n.hookSpecificOutput,i=[];if(t===`UserPromptSubmit`){let e=B(r);if(n.decision===`block`)return{context:e?[e]:[],blockReason:`Blocked by hook (decision: block)`};e&&i.push(e)}let a,o;if(t===`PreToolUse`&&typeof r==`object`&&r){let e=r;if(z(e.permissionDecision)){if(a=e.permissionDecision,a===`deny`)return{context:[],blockReason:`Blocked by hook (permissionDecision: deny)`,permissionDecision:a};e.updatedInput!==void 0&&(o=e.updatedInput)}}return typeof n.systemMessage==`string`&&n.systemMessage&&i.push(n.systemMessage),{context:i,...a!==void 0&&{permissionDecision:a},...o!==void 0&&{updatedInput:o}}}function H(e){let t=e.trim();if(!t.startsWith(`{`))return null;try{return JSON.parse(t)}catch{return null}}function U(e,t){if(typeof e!=`object`||!e||Array.isArray(e))return null;let n=e;if(n.continue===!1)return typeof n.stopReason==`string`&&n.stopReason?n.stopReason:`Blocked by hook (continue: false)`;if(t===`UserPromptSubmit`&&n.decision===`block`)return typeof n.reason==`string`&&n.reason?n.reason:`Blocked by hook (decision: block)`;if(t===`PreToolUse`){let e=n.hookSpecificOutput;if(typeof e==`object`&&e&&e.permissionDecision===`deny`)return`Blocked by hook (permissionDecision: deny)`}return null}function W(e){let t=e.replace(/\s+/g,` `).trim();return t.length>200?`${t.slice(0,200)}…`:t}function G(e,t,n){let r;try{r=JSON.parse(e)}catch(n){return{outcome:`error`,source:t,kind:`malformed-response`,reason:`Hook response is not valid JSON (${n instanceof Error?n.message:String(n)}): ${W(e)}`}}return K(r,t,n,e)}function K(e,t,n,r){if(typeof e!=`object`||!e||Array.isArray(e))return{outcome:`error`,source:t,kind:`malformed-response`,reason:`Hook response is not a JSON object: ${W(r)}`};let{ok:i,reason:a}=e;if(i===!0)return{outcome:`allow`,source:t,stdout:r};if(i===!1)return{outcome:`deny`,source:t,reason:typeof a==`string`&&a?a:`Blocked by ${t} hook`};let o=U(e,n);return o===null?{outcome:`error`,source:t,kind:`malformed-response`,reason:i===void 0?`Hook response has no boolean "ok" field: ${W(r)}`:`Hook response "ok" is ${typeof i}, not boolean: ${W(r)}`}:{outcome:`deny`,source:t,reason:o}}Object.defineProperty(exports,"A",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"C",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"D",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"E",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"O",{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,"S",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"T",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"_",{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,"b",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return N}}),Object.defineProperty(exports,"g",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"h",{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return L}}),Object.defineProperty(exports,"j",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"k",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,"m",{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return R}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,"p",{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return V}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return G}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,"v",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"w",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"x",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"y",{enumerable:!0,get:function(){return t}});
|
|
1
|
+
var e=class extends Error{context;constructor(e,t){super(e),this.context=t,this.name=this.constructor.name,Object.setPrototypeOf(this,new.target.prototype)}},t=class extends e{code=`CONFIGURATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t){super(`Configuration Error: ${e}`,t)}},n=class extends e{field;code=`VALIDATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t,n){super(`Validation Error: ${e}`,n),this.field=t}},r=class extends e{issues;attempts;code=`STRUCTURED_OUTPUT_ERROR`;category=`provider`;recoverable=!0;constructor(e,t,n,r){super(`Structured Output Error: ${e}`,r),this.issues=t,this.attempts=n}},i=class extends e{provider;originalError;code=`PROVIDER_ERROR`;category=`provider`;recoverable=!0;status;type;constructor(e,t,n,r,i){super(`Provider Error (${t}): ${e}`,r),this.provider=t,this.originalError=n,i?.status!==void 0&&(this.status=i.status),i?.type!==void 0&&(this.type=i.type)}},a=class extends e{provider;code=`AUTHENTICATION_ERROR`;category=`user`;recoverable=!1;constructor(e,t,n){super(`Authentication Error: ${e}`,n),this.provider=t}},o=class extends e{retryAfter;provider;code=`RATE_LIMIT_ERROR`;category=`provider`;recoverable=!0;constructor(e,t,n,r){super(`Rate Limit Error: ${e}`,r),this.retryAfter=t,this.provider=n}},s=class extends e{originalError;provider;code=`NETWORK_ERROR`;category=`system`;recoverable=!0;constructor(e,t,n,r){super(`Network Error: ${e}`,n),this.originalError=t,this.provider=r}},c=class extends e{toolName;originalError;code=`TOOL_EXECUTION_ERROR`;category=`system`;recoverable=!1;constructor(e,t,n,r){super(`Tool Execution Error (${t}): ${e}`,r),this.toolName=t,this.originalError=n}},l=class extends e{toolName;callCount;maxSameToolInputs;code=`SAME_TOOL_INPUT_LOOP`;category=`system`;recoverable=!0;constructor(e,t,n,r){super(`Tool "${e}" was called with identical input ${t} times, past the maxSameToolInputs limit of ${n} — stopping the turn to break the loop`,r),this.toolName=e,this.callCount=t,this.maxSameToolInputs=n}},u=class extends e{round;code=`EMPTY_COMPLETION`;category=`provider`;recoverable=!0;constructor(e,t){super(`Round ${e} ended with no tool call and no text response, and allowToolOnlyCompletion prevented the follow-up call that would otherwise recover it`,t),this.round=e}},d=class extends e{model;provider;availableModels;code=`MODEL_NOT_AVAILABLE`;category=`user`;recoverable=!1;constructor(e,t,n,r){let i=e===void 0?`No model available for provider "${t}"`:`Model "${e}" is not available for provider "${t}"`,a=r?.originalMessage;super(typeof a==`string`&&a.length>0?`${i}: ${a}`:i,r),this.model=e,this.provider=t,this.availableModels=n}},f=class extends e{code=`CIRCUIT_BREAKER_OPEN`;category=`system`;recoverable=!0;constructor(e=`Circuit breaker is open`,t){super(e,t)}},p=class extends e{pluginName;code=`PLUGIN_ERROR`;category=`system`;recoverable=!1;constructor(e,t,n){super(`Plugin Error (${t}): ${e}`,n),this.pluginName=t}},m=class extends e{code=`STORAGE_ERROR`;category=`system`;recoverable=!0;constructor(e,t){super(`Storage Error: ${e}`,t)}},h=class extends e{code=`CACHE_INTEGRITY_ERROR`;category=`system`;recoverable=!1;constructor(e,t){super(`Cache Integrity Error: ${e}`,t)}},g=class extends e{executable;code=`UNSUPPORTED_SHELL`;category=`user`;recoverable=!1;constructor(e){super(`Unsupported shell executable: ${e}`,{executable:e}),this.executable=e}};function _(e){return e===`darwin`?'Host OS: macOS (POSIX, BSD userland). Use BSD-flavored utilities: `sed -i ""` needs an empty backup arg, `date`/`grep`/`stat` flags differ from GNU. Prefer portable POSIX flags; avoid Linux/GNU-only options.':e===`linux`?"Host OS: Linux (POSIX, usually GNU coreutils, but distro/user setup varies). Prefer portable POSIX flags; do not assume a specific distro, and probe with `command -v` before relying on a non-standard tool.":`Host OS: ${e}; active shell family: POSIX sh/bash. Write portable POSIX syntax and avoid host-specific flags.`}function v(e,t){let n=x(e)===`bash`?`bash`:`sh`;return{command:e,kind:n,platform:t,commandArgs:e=>[`-c`,e],interactiveArgs:[],label:`${n} on ${t}`,syntaxHint:_(t)}}function y(e,t){return{command:e,kind:`powershell`,platform:t,commandArgs:e=>[`-NoProfile`,`-Command`,e],interactiveArgs:[`-NoProfile`],label:`PowerShell on ${t}`,syntaxHint:`Host OS: ${t}; active shell family: PowerShell. Write PowerShell syntax (not bash): e.g. \`Get-ChildItem\` not \`ls -la\`, \`\$env:VAR\` not \`\$VAR\`.`}}function b(e,t){return{command:e,kind:`cmd`,platform:t,commandArgs:e=>[`/d`,`/s`,`/c`,e],interactiveArgs:[],label:`cmd.exe on ${t}`,syntaxHint:`Host OS: ${t}; active shell family: Windows cmd.exe. Write cmd.exe syntax.`}}function x(e){return(e.split(/[\\/]/).at(-1)?.toLowerCase()??``).replace(/\.exe$/i,``)}function S(e,t){switch(x(e)){case`sh`:case`bash`:return v(e,t);case`powershell`:case`pwsh`:return y(e,t);case`cmd`:return b(e,t);default:throw new g(e)}}function C(e){let t=e?.trim();return t===void 0||t.length===0?void 0:t}function w(e={}){let t=e.env??process.env,n=e.platform??process.platform,r=C(e.executable);return r===void 0?n===`win32`?y(`powershell.exe`,n):v(C(t.SHELL)??`/bin/sh`,n):S(r,n)}const T=/^(?!0{32}$)[0-9a-f]{32}$/,E=/^(?!0{16}$)[0-9a-f]{16}$/;function D(e){return e.replaceAll(`-`,``).slice(0,16)}const O=D;function k(e){return E.test(e)}function A(e,t){if(!(!T.test(e)||!E.test(t)))return`00-${e}-${t}-01`}function j(e,t){if(e.allowedOrigins.length===0)return;let n=A(e.traceId,D(t));if(n!==void 0)return{traceparent:n,allowedOrigins:[...e.allowedOrigins]}}function M(e,t){return j(e,t)}function N(e,t,n){if(t?.subprocessClasses?.includes(e)!==!0)return;let r=A(t.traceId,n);return r===void 0?void 0:{TRACEPARENT:r}}function P(e,t,n={}){if(t===void 0||n.TRACEPARENT!==void 0)return{...e,...n};let{TRACESTATE:r,...i}=e;return{...i,...t,...n}}function F(e,t){if(e===void 0||t===void 0)return{};let n;try{n=new URL(e).origin}catch{return{}}return n===`null`||!t.allowedOrigins.includes(n)?{}:{traceparent:t.traceparent}}const I=e=>`\n…[output truncated: ${e} byte(s) dropped]`;function L(e){let t=e.retain??`head`,n=e.truncationMarker??I,r=Math.max(0,Math.floor(e.maxBytes)),i=new TextEncoder,a=[],o=0,s=0,c=e=>{let t=r-o;if(t<=0)return;let n=e.byteLength<=t?e:e.subarray(0,t);a.push(n),o+=n.byteLength},l=e=>{let t=e.byteLength<=r?e:e.subarray(e.byteLength-r);for(a.push(t),o+=t.byteLength;o>r&&a.length>0;){let e=a[0],t=o-r;e.byteLength<=t?(a.shift(),o-=e.byteLength):(a[0]=e.subarray(t),o-=t)}};return{append(e){let n=typeof e==`string`?i.encode(e):e;s+=n.byteLength,t===`head`?c(n):l(n)},get retainedBytes(){return o},get totalBytes(){return s},get droppedBytes(){return s-o},get truncated(){return s>o},toString(){let e=new Uint8Array(o),r=0;for(let t of a)e.set(t,r),r+=t.byteLength;a=a.length>1?[e]:a;let i=new TextDecoder().decode(e);if(s<=o)return i;let c=n(s-o);return t===`head`?`${i}${c}`:`${c.replace(/^\n/,``)}\n${i}`}}}const R={deny:3,ask:2,defer:1,allow:0};function z(e){return typeof e==`string`&&e in R}function B(e){if(!(typeof e!=`object`||!e)&&`additionalContext`in e)return String(e.additionalContext)}function V(e,t){let n=H(e);if(n===null){let t=e.trim();return{context:t?[t]:[]}}if(n.continue===!1)return{context:[],blockReason:typeof n.stopReason==`string`?n.stopReason:`Blocked by hook (continue: false)`};let r=n.hookSpecificOutput,i=[];if(t===`UserPromptSubmit`){let e=B(r);if(n.decision===`block`)return{context:e?[e]:[],blockReason:`Blocked by hook (decision: block)`};e&&i.push(e)}let a,o;if(t===`PreToolUse`&&typeof r==`object`&&r){let e=r;if(z(e.permissionDecision)){if(a=e.permissionDecision,a===`deny`)return{context:[],blockReason:`Blocked by hook (permissionDecision: deny)`,permissionDecision:a};e.updatedInput!==void 0&&(o=e.updatedInput)}}return typeof n.systemMessage==`string`&&n.systemMessage&&i.push(n.systemMessage),{context:i,...a!==void 0&&{permissionDecision:a},...o!==void 0&&{updatedInput:o}}}function H(e){let t=e.trim();if(!t.startsWith(`{`))return null;try{return JSON.parse(t)}catch{return null}}function U(e,t){if(typeof e!=`object`||!e||Array.isArray(e))return null;let n=e;if(n.continue===!1)return typeof n.stopReason==`string`&&n.stopReason?n.stopReason:`Blocked by hook (continue: false)`;if(t===`UserPromptSubmit`&&n.decision===`block`)return typeof n.reason==`string`&&n.reason?n.reason:`Blocked by hook (decision: block)`;if(t===`PreToolUse`){let e=n.hookSpecificOutput;if(typeof e==`object`&&e&&e.permissionDecision===`deny`)return`Blocked by hook (permissionDecision: deny)`}return null}function W(e){let t=e.replace(/\s+/g,` `).trim();return t.length>200?`${t.slice(0,200)}…`:t}function G(e,t,n){let r;try{r=JSON.parse(e)}catch(n){return{outcome:`error`,source:t,kind:`malformed-response`,reason:`Hook response is not valid JSON (${n instanceof Error?n.message:String(n)}): ${W(e)}`}}return K(r,t,n,e)}function K(e,t,n,r){if(typeof e!=`object`||!e||Array.isArray(e))return{outcome:`error`,source:t,kind:`malformed-response`,reason:`Hook response is not a JSON object: ${W(r)}`};let{ok:i,reason:a}=e;if(i===!0)return{outcome:`allow`,source:t,stdout:r};if(i===!1)return{outcome:`deny`,source:t,reason:typeof a==`string`&&a?a:`Blocked by ${t} hook`};let o=U(e,n);return o===null?{outcome:`error`,source:t,kind:`malformed-response`,reason:i===void 0?`Hook response has no boolean "ok" field: ${W(r)}`:`Hook response "ok" is ${typeof i}, not boolean: ${W(r)}`}:{outcome:`deny`,source:t,reason:o}}Object.defineProperty(exports,"A",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"C",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"D",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"E",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"O",{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,"S",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"T",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"_",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,"b",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return N}}),Object.defineProperty(exports,"g",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"h",{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return L}}),Object.defineProperty(exports,"j",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"k",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,"m",{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return R}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,"p",{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return V}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return G}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,"v",{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,"w",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"x",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"y",{enumerable:!0,get:function(){return f}});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-core",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.88",
|
|
4
4
|
"description": "Foundation of the Robota SDK: the Robota agent engine and the provider, tool, plugin and permission contracts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -84,15 +84,6 @@
|
|
|
84
84
|
},
|
|
85
85
|
"./package.json": "./package.json"
|
|
86
86
|
},
|
|
87
|
-
"repository": {
|
|
88
|
-
"type": "git",
|
|
89
|
-
"url": "https://github.com/woojubb/robota.git",
|
|
90
|
-
"directory": "packages/agent-core"
|
|
91
|
-
},
|
|
92
|
-
"homepage": "https://docs.robota.io/en/packages/agent-core/",
|
|
93
|
-
"bugs": {
|
|
94
|
-
"url": "https://github.com/woojubb/robota/issues"
|
|
95
|
-
},
|
|
96
87
|
"engines": {
|
|
97
88
|
"node": ">=22.12.0"
|
|
98
89
|
},
|
|
@@ -134,6 +125,7 @@
|
|
|
134
125
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
135
126
|
"dependencies": {
|
|
136
127
|
"jssha": "^3.3.2",
|
|
128
|
+
"undici": "7.29.1",
|
|
137
129
|
"zod": "^3.25.76"
|
|
138
130
|
},
|
|
139
131
|
"devDependencies": {
|
|
@@ -154,6 +146,14 @@
|
|
|
154
146
|
"volta": {
|
|
155
147
|
"extends": "../../package.json"
|
|
156
148
|
},
|
|
149
|
+
"repository": {
|
|
150
|
+
"type": "git",
|
|
151
|
+
"url": "https://github.com/woojubb/robota"
|
|
152
|
+
},
|
|
153
|
+
"bugs": {
|
|
154
|
+
"url": "https://github.com/woojubb/robota/issues"
|
|
155
|
+
},
|
|
156
|
+
"homepage": "https://robota.io/",
|
|
157
157
|
"scripts": {
|
|
158
158
|
"build": "rimraf dist && tsdown",
|
|
159
159
|
"build:js": "pnpm run build",
|