{"version":3,"file":"safe-merge.mjs","names":[],"sources":["../../../../../../../ai/src/security/safe-merge.ts"],"sourcesContent":["/**\n * Prototype-key guard for merges of model-influenced data into plain\n * objects (supervisor `state`, artifact bags, refine slices, …).\n *\n * Any `target[key] = value` where `key` came from an LLM, a tool\n * result, or a permissively-schema'd agent output is a prototype-\n * tampering primitive: `state[\"__proto__\"] = {...}` repoints that\n * object's prototype, and `state[\"constructor\"]` shadows its\n * constructor. On a plain object literal the blast radius is contained\n * (the write lands on the one object, not on `Object.prototype`), but\n * it becomes real prototype pollution the moment anything downstream\n * uses `in`, `hasOwnProperty`, or a recursive deep-merge on the\n * tainted object — which is exactly the kind of change that gets added\n * later without re-auditing the merge sites.\n *\n * So: one shared guard, applied at every merge boundary, dropping the\n * dangerous keys instead of assigning them. Dropping (not throwing) is\n * deliberate — these keys are never legitimate state fields, and a\n * merge boundary in the middle of a settled iteration is the wrong\n * place to fail a run. Callers get the dropped keys back so they can\n * log the anomaly.\n */\n\n/**\n * Keys that must never be written through a dynamic-key assignment.\n * `__proto__` repoints the prototype; `constructor` / `prototype`\n * are the standard escalation path from there.\n */\nexport const UNSAFE_MERGE_KEYS: ReadonlyArray<string> = [\"__proto__\", \"constructor\", \"prototype\"];\n\nconst UNSAFE_MERGE_KEY_SET = new Set(UNSAFE_MERGE_KEYS);\n\n/**\n * True when `key` must not be assigned onto an object built from\n * untrusted (model/tool-influenced) data.\n */\nexport function isUnsafeMergeKey(key: string): boolean {\n  return UNSAFE_MERGE_KEY_SET.has(key);\n}\n\n/**\n * Assign one key onto `target`, skipping prototype-tampering keys.\n * Returns `true` when the value was written, `false` when the key was\n * refused.\n */\nexport function assignSafeKey(\n  target: Record<string, unknown>,\n  key: string,\n  value: unknown,\n): boolean {\n  if (isUnsafeMergeKey(key)) {\n    return false;\n  }\n\n  target[key] = value;\n\n  return true;\n}\n\n/**\n * Shallow-merge every own enumerable key of `source` into `target`,\n * skipping prototype-tampering keys. Mutates `target` in place (call\n * sites rely on external references to the merged object staying\n * coherent) and returns the list of refused keys — empty in the\n * overwhelmingly common case, non-empty only when something upstream\n * tried to smuggle `__proto__`/`constructor`/`prototype` through.\n */\nexport function mergeSafely(\n  target: Record<string, unknown>,\n  source: Record<string, unknown>,\n): string[] {\n  const skipped: string[] = [];\n\n  for (const [key, value] of Object.entries(source)) {\n    if (!assignSafeKey(target, key, value)) {\n      skipped.push(key);\n    }\n  }\n\n  return skipped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,oBAA2C;CAAC;CAAa;CAAe;AAAW;AAEhG,MAAM,uBAAuB,IAAI,IAAI,iBAAiB;;;;;AAMtD,SAAgB,iBAAiB,KAAsB;CACrD,OAAO,qBAAqB,IAAI,GAAG;AACrC;;;;;;AAOA,SAAgB,cACd,QACA,KACA,OACS;CACT,IAAI,iBAAiB,GAAG,GACtB,OAAO;CAGT,OAAO,OAAO;CAEd,OAAO;AACT;;;;;;;;;AAUA,SAAgB,YACd,QACA,QACU;CACV,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,cAAc,QAAQ,KAAK,KAAK,GACnC,QAAQ,KAAK,GAAG;CAIpB,OAAO;AACT"}