UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

156 lines 8.18 kB
/** * 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