eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
156 lines • 8.18 kB
TypeScript
/**
* Hardened introspection for serializing values that may originate inside a
* workflow VM (`node:vm`) sandbox realm.
*
* Serialization runs on the host, but the values it inspects were
* constructed by workflow code, so a naive dynamic operation like
* `value.toISOString()`, `Array.from(map)`, or `Object.prototype.toString`
* dispatches into the sandbox realm and executes workflow code (patched
* prototype methods, getters, proxy traps, `Symbol.toStringTag` accessors).
* That is a determinism hazard: serialization happens exactly once per
* payload (never again on replay), so any workflow-visible side effect it
* triggers exists only on the live execution path and diverges from replay.
*
* This module makes serialization side-effect free wherever the data allows
* it, and *observable* where it does not:
*
* - **Classification** uses engine-level brand checks (`node:util` `types`,
* internal-slot probes) instead of `instanceof` / `Object.prototype.toString`,
* which makes it immune to `Symbol.hasInstance`, `Symbol.toStringTag`, and
* reassigned globals.
* - **Extraction** goes through intrinsics captured at module load (host
* boot, before any workflow code runs). Internal slots are realm-agnostic,
* so host intrinsics read VM-realm objects without touching the sandbox's
* (patchable) prototypes.
* - **Property access** reads through descriptors, so plain data never
* invokes anything. Where workflow code *must* run because the data itself
* lives behind it (getters, proxies, custom `[WORKFLOW_SERIALIZE]`
* methods, `toString()` on toStringTag-branded objects such as Temporal
* polyfills), the execution is preserved for compatibility and recorded
* in the active {@link GuestCodeStats} sink, so callers (e.g. a retained-VM
* gate) can react.
*
* **Recording is not prevention.** For the recorded cases the determinism
* hazard is still live: a getter that calls `Math.random()` advances the
* run's seeded PRNG during serialization, and because serialization happens
* exactly once and is never replayed, every subsequent draw (including the
* correlation ids derived from that stream) shifts relative to replay. The
* report is the only trace of that; acting on it (warning, demoting a
* retained VM to replay) is left to the caller.
*
* The recorder is ambient module state, set for the duration of a
* synchronous `stringify` call via {@link withGuestCodeStats}. devalue's
* `stringify` is fully synchronous, so this is safe without async context.
*/
import type { StringifyOperations } from '../_devalue.js';
/**
* A single instance of workflow (guest) code executing during serialization.
*/
export interface GuestCodeExecution {
/**
* What forced the execution:
* - `getter`: an accessor property was invoked to read data
* - `proxy`: a proxy was introspected, firing its traps. Note this also
* implies a **shape change**: brand checks answer "not that type" for a
* proxy, so a proxied `Map` serializes as a plain object rather than as a
* `Map`, and this report is the only evidence of it. (Such values were
* never serializable before, since the internal-slot reads in the previous
* implementation threw on them, so the shape change replaces a crash,
* but it is silent.)
* - `method`: a workflow-defined function was invoked (e.g. a custom
* `[WORKFLOW_SERIALIZE]` serializer, `toString()` on a
* `Symbol.toStringTag`-branded object, a duck-typed `getTime()`, or a
* `__closureVarsFn` this package did not generate)
*/
kind: 'getter' | 'proxy' | 'method';
/** Best-effort context: the property key, method name, or tag involved. */
detail?: string;
}
/**
* Mutable sink recording workflow-code execution serialization could not
* avoid. `executions` contains the first five samples and `totalExecutions`
* is the exact count.
*/
export interface GuestCodeStats {
executions: GuestCodeExecution[];
totalExecutions?: number;
}
export declare const GUEST_CODE_EXECUTION_SAMPLE_LIMIT = 5;
/**
* Runs `fn` (synchronously) with `stats` as the active guest-code sink.
* Nested calls stack correctly; a `null`/`undefined` sink disables
* recording without disabling hardening.
*/
export declare function withGuestCodeStats<T>(stats: GuestCodeStats | undefined, fn: () => T): T;
/** Marks a function as having been passed to `useStep`. */
export declare function markUseStepClosureFn<T extends object>(fn: T): T;
/** Whether `fn` was marked by {@link markUseStepClosureFn}. */
export declare function isUseStepClosureFn(fn: unknown): boolean;
export declare function recordGuestCode(kind: GuestCodeExecution['kind'], detail?: string): void;
/** `Date.prototype.getDate`, for invalid-date checks. */
export declare const dateGetDate: (thisArg: unknown) => number;
/** `Date.prototype.getTime`. */
export declare const dateGetTime: (thisArg: unknown) => number;
/** `Date.prototype.toISOString`. */
export declare const dateToISOString: (thisArg: unknown) => string;
/** `RegExp.prototype.source` getter. */
export declare const regExpSource: (value: unknown) => string;
/** `RegExp.prototype.flags` getter. */
export declare const regExpFlags: (value: unknown) => string;
/** `ArrayBuffer.prototype.byteLength` getter (internal slot read). */
export declare const arrayBufferByteLength: (value: unknown) => number;
/**
* Reads `value[key]` with `[[Get]]` semantics, but through descriptors:
* plain data properties never invoke anything; accessor properties are
* invoked (the data lives behind them) and recorded; proxies fall back to
* a plain read (their traps are the only access path) and are recorded.
*/
export declare function readProperty(value: unknown, key: PropertyKey): unknown;
/**
* `key in value` semantics without firing proxy traps for ordinary
* objects. Proxies fall back to the `in` operator and are recorded.
*/
export declare function hasProperty(value: unknown, key: PropertyKey): boolean;
/**
* `value instanceof C` semantics for a known `C.prototype`, without
* consulting `Symbol.hasInstance` (which workflow code can define). Used
* for host classes that are injected into the sandbox (Headers, URL,
* URLSearchParams, DOMException), where the instances (from any realm the
* host handed the class to) carry the host prototype in their chain.
*
* Proxies are walked rather than rejected: `Reflect.getPrototypeOf` fires
* the proxy's `getPrototypeOf` trap, matching `instanceof` semantics, and
* real values depend on that: Next.js hands the runtime a proxied
* `NextRequest`, and answering "not a Request" for it would silently break
* webhooks. The traps are guest-observable, so the proxy is recorded.
*/
export declare function isInstanceOfPrototype(value: unknown, prototype: object | undefined): boolean;
/** `URL.prototype.href` getter. */
export declare function urlHref(value: URL): string;
/** `URLSearchParams.prototype.toString`: returns `''` iff empty. */
export declare function urlSearchParamsToString(value: URLSearchParams): string;
/**
* Iterates a Headers instance through the captured host iterator, so the
* iterator object, and its `next`, are host-realm.
*/
export declare function headersToEntries(value: Headers): [string, string][];
/**
* Iterates a genuine Map's entries entirely through host intrinsics: the
* iterator object is created by the host `Map.prototype.entries`, so its
* realm, and therefore its `next`, is the host's, not the sandbox's.
*/
export declare function mapToEntries(value: Map<unknown, unknown>): [unknown, unknown][];
/** See {@link mapToEntries}. */
export declare function setToValues(value: Set<unknown>): unknown[];
/**
* The bytes of an `ArrayBufferView`, read via internal-slot getters, so
* own-property shadowing and prototype patches cannot change which bytes
* are serialized.
*/
export declare function viewInfo(value: ArrayBufferView): {
buffer: ArrayBufferLike;
byteOffset: number;
byteLength: number;
};
export declare const hardenedStringifyOperations: Partial<StringifyOperations>;
//# sourceMappingURL=hardened.d.ts.map