UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

208 lines • 9.22 kB
"use strict"; /** * plainData — copy the JSON data domain out of an untrusted value. * * MCP `structuredContent` arrives from a transport the library does not own, * so the value may be anything: a class instance, a Proxy, an object with * getters, or a plain object built in another realm (jest's environment, * `node:vm`, an iframe). This module answers one question — is this plain JSON * data? — and, when it is, hands back a COPY made of this realm's own objects * and arrays, built from property descriptors read once each. * * Why a copy rather than the value itself: whatever the source does after it * was checked (a Proxy trap that redefines a property it already reported, a * foreign prototype whose methods were replaced) cannot reach the copy. The * copy keeps every own key as a data property (`__proto__` included), keeps a * null prototype null, and holds nothing else: no getter, no foreign prototype, no Proxy. * * Plain objects and arrays are recognised by STRUCTURE, not by identity with * this realm's `Object.prototype`, because each realm has its own intrinsics: * * - `Object.prototype` of a realm: its own prototype is null; * - `Array.prototype` of a realm: an array whose own prototype is a realm's * `Object.prototype`; * - its own `constructor` is a data property holding that realm's native * `Object` / `Array` (source text `function Object() { [native code] }`), * whose own `prototype` data property points straight back at it — a link * a user-made object or a Proxy cannot forge, because a native * constructor's `prototype` is non-writable and non-configurable; * - it carries the same own keys as this realm's intrinsic, each with the * same descriptor kind (data or accessor), so an added getter is refused; * - `Object.prototype.toString` names it `[object Object]` / `[object Array]`. * * Only descriptors are read: no getter, `toJSON` or `valueOf` runs. A Proxy is * refused before any trap runs wherever the runtime can say what a Proxy is * (Node's `util.types.isProxy`, reached through `process.getBuiltinModule` at * call time, so nothing is imported and the module stays browser-clean). * Elsewhere a Proxy's traps answer the walk like any object's descriptors do, * and only what they answered reaches the copy. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.copyJsonObject = void 0; const LOCAL = { Object: Object.prototype, Array: Array.prototype, }; /** * A realm's own constructor, as `Function.prototype.toString` prints it. V8 * prints `function Object() { [native code] }`; SpiderMonkey and JavaScriptCore * put the body on its own lines, so whitespace inside the braces is free. */ const NATIVE_SOURCE = { Object: /^function Object\(\) \{\s*\[native code\]\s*\}$/, Array: /^function Array\(\) \{\s*\[native code\]\s*\}$/, }; /** The documented decoding bounds. */ const MAX_VALUES = 1_000_000; const MAX_DEPTH = 64; /** * A copy of `value` when it is a JSON object (not an array, not a scalar) * within the bounds, made of this realm's objects; `undefined` otherwise. * Never throws for a hostile value: any trap or check that throws refuses. */ function copyJsonObject(value) { try { if (value === null || typeof value !== 'object' || isProxyValue(value)) return undefined; if (Array.isArray(value)) return undefined; const copied = new JsonCopier().copy(value, 0); return copied === undefined ? undefined : copied; } catch { return undefined; } } exports.copyJsonObject = copyJsonObject; /** * One walk: its visit counter and its cycle guard. * * No memo for shared sub-objects, on purpose: each path is walked and counted, * so the bounds hold for what the copy really contains (a memo let a DAG of a * few hundred references stand for a copy of 2^60 nodes). */ class JsonCopier { visited = 0; ancestors = new Set(); verified = new WeakSet(); /** The copy of `item`, or `undefined` when it is not JSON data. */ copy(item, depth) { if (++this.visited > MAX_VALUES) return undefined; if (item === null || typeof item === 'string' || typeof item === 'boolean') return item; if (typeof item === 'number') return Number.isFinite(item) ? item : undefined; if (typeof item !== 'object' || depth >= MAX_DEPTH || this.ancestors.has(item)) return undefined; if (isProxyValue(item)) return undefined; return this.copyContainer(item, depth); } copyContainer(item, depth) { const array = Array.isArray(item); const prototype = Object.getPrototypeOf(item); const plain = array ? isIntrinsicPrototype(prototype, 'Array', this.verified) : prototype === null || isIntrinsicPrototype(prototype, 'Object', this.verified); if (!plain) return undefined; const keys = Reflect.ownKeys(item); // The length descriptor, not `item.length`: a Get could run code. const length = array ? Object.getOwnPropertyDescriptor(item, 'length')?.value : 0; if (typeof length !== 'number' || (array && keys.length !== length + 1)) return undefined; const out = array ? [] : prototype === null ? Object.create(null) : {}; this.ancestors.add(item); try { for (const key of keys) { if (array && key === 'length') continue; if (typeof key !== 'string') return undefined; if (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length)) return undefined; const descriptor = Object.getOwnPropertyDescriptor(item, key); if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor)) return undefined; const value = this.copy(descriptor.value, depth + 1); if (value === undefined) return undefined; // A data property of its own, whatever the key: `__proto__` stays a key. Object.defineProperty(out, key, { value, writable: true, enumerable: true, configurable: true, }); } return out; } finally { this.ancestors.delete(item); } } } function isIntrinsicPrototype(proto, name, verified) { if (proto === LOCAL[name]) return true; if (typeof proto !== 'object' || proto === null || isProxyValue(proto)) return false; if (verified.has(proto)) return true; const parent = Object.getPrototypeOf(proto); const parentOk = name === 'Object' ? parent === null : isIntrinsicPrototype(parent, 'Object', verified); if (!parentOk || Array.isArray(proto) !== (name === 'Array')) return false; if (!nativeConstructorLinksBack(proto, name) || !sameShape(proto, LOCAL[name])) return false; // Safe only now: the shape matches this realm's intrinsic, which has no own // Symbol.toStringTag, and the parent chain is verified, so no getter runs. if (Object.prototype.toString.call(proto) !== `[object ${name}]`) return false; verified.add(proto); return true; } function nativeConstructorLinksBack(proto, name) { const ctor = Object.getOwnPropertyDescriptor(proto, 'constructor'); if (ctor === undefined || !('value' in ctor) || typeof ctor.value !== 'function') return false; // Function.prototype.toString never runs user code; a Proxy or bound function // prints an anonymous native body, a class prints its source. if (!NATIVE_SOURCE[name].test(Function.prototype.toString.call(ctor.value))) return false; const back = Object.getOwnPropertyDescriptor(ctor.value, 'prototype'); return back !== undefined && 'value' in back && back.value === proto; } function sameShape(candidate, local) { const keys = Reflect.ownKeys(local); if (Reflect.ownKeys(candidate).length !== keys.length) return false; for (const key of keys) { const theirs = Object.getOwnPropertyDescriptor(candidate, key); const ours = Object.getOwnPropertyDescriptor(local, key); if (theirs === undefined || ours === undefined || 'value' in theirs !== 'value' in ours) return false; } return true; } let proxyCheck; /** Node's `util.types.isProxy`, looked up once; runs no trap. False where absent. */ function isProxyValue(value) { if (proxyCheck === undefined) { proxyCheck = null; try { const host = globalThis .process; const util = host?.getBuiltinModule?.('node:util'); const check = util?.types?.isProxy; if (typeof check === 'function') proxyCheck = check; } catch { proxyCheck = null; } } return proxyCheck !== null && proxyCheck(value); } //# sourceMappingURL=plainData.js.map