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
JavaScript
;
/**
* 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