AID-I-007: Client-Side AI Execution Isolation

Version 1.20261015 · Actionable technique

This technique focuses on containing a compromised or malicious client-side model, preventing it from accessing sensitive data from other browser tabs, local application context, or the operating system. It addresses the security challenges of AI models that execute in untrusted environments like a user's web browser, Electron shell, hybrid mobile app, or native mobile runtime. This assumes the model or model runtime may already be tampered with or coerced (e.g. prompt-injected, modified weights, wrapped with hostile JS). The goal is not to "fix" the model but to strictly confine its blast radius using sandboxing, least capability, and controlled IPC.

Pillars: app

Lifecycle: operation

Scope boundary

Owns component-level confinement of a client-side AI model or runtime through Web Workers, sandboxed iframes, WASM capabilities, CSP, and minimal native bridges inside browsers, Electron shells, hybrid apps, or mobile runtimes. It does not own browser-agent task lifecycle or browser-managed and desktop-workspace state.

AID-I-008: AID-I-007 isolates the embedded client-side model or runtime component; AID-I-008 isolates browser-agent and desktop computer-use state by task or trust zone. Cookies, localStorage, IndexedDB, cache, history, downloads, clipboard, magic links, desktop profiles, and workspace residue belong to I-008 even when the model component is separately confined here.

AID-I-001: AID-I-007 confines a model inside an end-user client application; AID-I-001 isolates managed AI execution in containers, sandboxes, or microVMs. A client-side Worker or iframe boundary is not evidence that a server-side tool or model workload runs in the managed sandbox family.

AID-H-003.011: AID-I-007 confines the client-side model runtime and its access to browser, application, operating-system, and native-bridge capabilities after execution begins; AID-H-003.011 decides whether an entitled, freshly attested client may receive the decryption key for an exact protected model release. Client isolation does not authorize model delivery, and key-release admission does not prove post-release runtime confinement.

Implementation guidance

AID-I-007-G001

Execute AI models in a dedicated Web Worker.
Security boundary and limits

A dedicated worker removes direct DOM authority and gives the parent a controllable message boundary. It is not a network or storage sandbox: a worker can use worker-available fetch, WebSocket, IndexedDB, and other APIs subject to the page's effective policy. Use AID-I-007-G004 and the surrounding runtime/network controls for egress. PASS here proves only that a verified worker artifact received no privileged handles and every parent/worker exchange followed the signed protocol.

Verified worker and model launcher
// File: verified-worker-launcher.js
// Inputs policy and evidenceClient come from trusted, non-model-controlled adapters.
// policy fields: workerUrl, workerSha256, runtimeUrl, runtimeSha256,
// modelUrl, modelSha256, maxArtifactBytes, maxInputValues,
// maxOutputValues, artifactFetchTimeoutMs, operationTimeoutMs, policySha256.
const textEncoder = new TextEncoder();

function hex(bytes) {
  return Array.from(new Uint8Array(bytes))
    .map((value) => value.toString(16).padStart(2, "0"))
    .join("");
}

function requirePositivePolicyInteger(value, field) {
  if (!Number.isSafeInteger(value) || value <= 0) {
    throw new Error(field + " must be a signed positive safe integer");
  }
}

function requireNonzeroSha256(value, field) {
  if (!/^[0-9a-f]{64}$/.test(value) || /^0{64}$/.test(value)) {
    throw new Error(field + " must be a nonzero lowercase SHA-256 digest");
  }
}

async function fetchVerified(
  urlText,
  expectedSha256,
  maxBytes,
  fetchTimeoutMs
) {
  requirePositivePolicyInteger(maxBytes, "maxArtifactBytes");
  requirePositivePolicyInteger(fetchTimeoutMs, "artifactFetchTimeoutMs");
  requireNonzeroSha256(expectedSha256, "artifact digest");
  const requested = new URL(urlText, location.href);
  if (
    location.protocol !== "https:" ||
    requested.protocol !== "https:" ||
    requested.username ||
    requested.password ||
    requested.hash ||
    requested.origin !== location.origin
  ) {
    throw new Error("worker artifact must use the canonical application HTTPS origin");
  }
  if (typeof AbortSignal.timeout !== "function") {
    throw new Error("deadline-capable AbortSignal is unavailable");
  }
  const response = await fetch(requested, {
    cache: "no-store",
    credentials: "omit",
    redirect: "error",
    signal: AbortSignal.timeout(fetchTimeoutMs)
  });
  if (!response.ok || !response.body || response.url !== requested.href) {
    throw new Error("artifact fetch failed or redirected");
  }
  const reader = response.body.getReader();
  const chunks = [];
  let length = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    length += value.byteLength;
    if (length > maxBytes) {
      await reader.cancel();
      throw new Error("artifact exceeds signed policy");
    }
    chunks.push(value);
  }
  const bytes = new Uint8Array(length);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  const observed = hex(await crypto.subtle.digest("SHA-256", bytes));
  if (observed !== expectedSha256) {
    throw new Error("artifact digest mismatch");
  }
  return bytes;
}

function randomId() {
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  return hex(bytes);
}

function validateFiniteVector(value, maxValues) {
  if (
    !Array.isArray(value) ||
    value.length === 0 ||
    value.length > maxValues ||
    value.some((item) => typeof item !== "number" || !Number.isFinite(item))
  ) {
    throw new Error("invalid numeric vector");
  }
  return value;
}

function canonicalJson(value) {
  if (value === null || typeof value === "boolean" || typeof value === "string") {
    return JSON.stringify(value);
  }
  if (typeof value === "number" && Number.isFinite(value)) {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return "[" + value.map(canonicalJson).join(",") + "]";
  }
  if (value && typeof value === "object") {
    return "{" + Object.keys(value).sort().map((key) =>
      JSON.stringify(key) + ":" + canonicalJson(value[key])
    ).join(",") + "}";
  }
  throw new Error("evidence value is not canonical JSON");
}

async function digestValue(value) {
  const bytes = textEncoder.encode(canonicalJson(value));
  return "sha256:" + hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
}

export async function startVerifiedWorker(policy, evidenceClient) {
  requirePositivePolicyInteger(policy.maxArtifactBytes, "maxArtifactBytes");
  requirePositivePolicyInteger(policy.maxInputValues, "maxInputValues");
  requirePositivePolicyInteger(policy.maxOutputValues, "maxOutputValues");
  requirePositivePolicyInteger(
    policy.artifactFetchTimeoutMs,
    "artifactFetchTimeoutMs"
  );
  requirePositivePolicyInteger(policy.operationTimeoutMs, "operationTimeoutMs");
  requirePositivePolicyInteger(policy.generation, "generation");
  if (policy.operationTimeoutMs > 2_147_483_647) {
    throw new Error("operationTimeoutMs exceeds the browser timer range");
  }
  requireNonzeroSha256(policy.policySha256, "policySha256");
  const [runtimeBytes, controllerBytes, modelBytes] = await Promise.all([
    fetchVerified(
      policy.runtimeUrl,
      policy.runtimeSha256,
      policy.maxArtifactBytes,
      policy.artifactFetchTimeoutMs
    ),
    fetchVerified(
      policy.workerUrl,
      policy.workerSha256,
      policy.maxArtifactBytes,
      policy.artifactFetchTimeoutMs
    ),
    fetchVerified(
      policy.modelUrl,
      policy.modelSha256,
      policy.maxArtifactBytes,
      policy.artifactFetchTimeoutMs
    )
  ]);

  // ort.min.js and the controller are version-pinned, verified, and concatenated
  // into one self-contained classic worker. The controller must not import code.
  const workerBlob = new Blob(
    [runtimeBytes, textEncoder.encode("\n"), controllerBytes],
    { type: "text/javascript" }
  );
  const workerObjectUrl = URL.createObjectURL(workerBlob);
  const worker = new Worker(workerObjectUrl, { name: "verified-ai-worker" });
  const channel = new MessageChannel();
  const sessionId = randomId();
  let nextSequence = 1;
  let closed = false;
  const pending = new Map();

  const appendVerifiedEvidence = async (event) => {
    const eventSha256 = await digestValue(event);
    const receipt = await evidenceClient.appendAndVerify(event);
    if (!receipt || receipt.eventSha256 !== eventSha256) {
      throw new Error("worker evidence receipt does not bind the exact event");
    }
    return receipt;
  };

  const destroy = async (reason) => {
    if (closed) return;
    closed = true;
    for (const item of pending.values()) {
      clearTimeout(item.timer);
      item.reject(new Error("worker terminated: " + reason));
    }
    pending.clear();
    channel.port1.close();
    worker.terminate();
    URL.revokeObjectURL(workerObjectUrl);
    await appendVerifiedEvidence({
      type: "worker_terminated",
      sessionId,
      reason,
      workerSha256: policy.workerSha256,
      runtimeSha256: policy.runtimeSha256,
      modelSha256: policy.modelSha256,
      policySha256: policy.policySha256,
      policyGeneration: policy.generation,
      parentOrigin: location.origin,
      terminalState: "TERMINATED"
    });
  };

  worker.addEventListener("error", () => {
    void destroy("worker_error");
  });
  worker.addEventListener("messageerror", () => {
    void destroy("bootstrap_messageerror");
  });

  channel.port1.onmessage = async (event) => {
    const message = event.data;
    if (
      !message ||
      message.type !== "RESULT" ||
      message.sessionId !== sessionId ||
      !Number.isSafeInteger(message.sequence)
    ) {
      void destroy("protocol_violation");
      return;
    }
    const waiter = pending.get(message.sequence);
    if (!waiter) {
      void destroy("unknown_or_replayed_sequence");
      return;
    }
    try {
      const output = validateFiniteVector(
        message.output, policy.maxOutputValues
      );
      const responseSha256 = await digestValue(output);
      await appendVerifiedEvidence({
        type: "worker_exchange",
        sessionId,
        sequence: message.sequence,
        requestSha256: waiter.requestSha256,
        responseSha256,
        workerSha256: policy.workerSha256,
        runtimeSha256: policy.runtimeSha256,
        modelSha256: policy.modelSha256,
        policySha256: policy.policySha256,
        policyGeneration: policy.generation,
        parentOrigin: location.origin,
        terminalState: "ACTIVE"
      });
      pending.delete(message.sequence);
      clearTimeout(waiter.timer);
      waiter.resolve(output);
    } catch (error) {
      void destroy("invalid_output");
      waiter.reject(error);
    }
  };
  channel.port1.onmessageerror = () => {
    void destroy("port_messageerror");
  };
  channel.port1.start();

  worker.postMessage({
    type: "BOOTSTRAP",
    sessionId,
    policy: {
      maxInputValues: policy.maxInputValues,
      maxOutputValues: policy.maxOutputValues
    },
    modelBytes: modelBytes.buffer
  }, [channel.port2, modelBytes.buffer]);

  await appendVerifiedEvidence({
    type: "worker_started",
    sessionId,
    workerSha256: policy.workerSha256,
    runtimeSha256: policy.runtimeSha256,
    modelSha256: policy.modelSha256,
    artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
    policySha256: policy.policySha256,
    policyGeneration: policy.generation,
    parentOrigin: location.origin
  });

  return {
    sessionId,
    async predict(input) {
      if (closed) throw new Error("worker is closed");
      const vector = validateFiniteVector(input, policy.maxInputValues);
      const requestSha256 = await digestValue(vector);
      const sequence = nextSequence++;
      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          pending.delete(sequence);
          reject(new Error("worker operation timed out"));
          void destroy("operation_timeout");
        }, policy.operationTimeoutMs);
        pending.set(sequence, { resolve, reject, timer, requestSha256 });
        channel.port1.postMessage({
          type: "PREDICT", sessionId, sequence, input: vector
        });
      });
    },
    close: () => destroy("controller_close")
  };
}
Self-contained worker controller
// File: verified-worker-controller.js
// Bundle and digest-pin this file. ort.min.js is concatenated before it.
"use strict";
let port = null;
let sessionId = null;
let session = null;
let limits = null;
let lastSequence = 0;

self.onmessage = async (event) => {
  const message = event.data;
  if (
    port ||
    !message ||
    message.type !== "BOOTSTRAP" ||
    event.ports.length !== 1 ||
    !(message.modelBytes instanceof ArrayBuffer)
  ) {
    self.close();
    return;
  }
  sessionId = message.sessionId;
  limits = message.policy;
  port = event.ports[0];
  try {
    session = await self.ort.InferenceSession.create(message.modelBytes, {
      executionProviders: ["wasm"]
    });
  } catch {
    self.close();
    return;
  }

  port.onmessage = async (requestEvent) => {
    const request = requestEvent.data;
    if (
      !request ||
      request.type !== "PREDICT" ||
      request.sessionId !== sessionId ||
      !Number.isSafeInteger(request.sequence) ||
      request.sequence !== lastSequence + 1 ||
      !Array.isArray(request.input) ||
      request.input.length === 0 ||
      request.input.length > limits.maxInputValues ||
      request.input.some(
        (item) => typeof item !== "number" || !Number.isFinite(item)
      )
    ) {
      self.close();
      return;
    }
    lastSequence = request.sequence;
    try {
      const inputName = session.inputNames[0];
      const tensor = new self.ort.Tensor(
        "float32",
        Float32Array.from(request.input),
        [1, request.input.length]
      );
      const results = await session.run({ [inputName]: tensor });
      const output = Array.from(results[session.outputNames[0]].data);
      if (
        output.length > limits.maxOutputValues ||
        output.some((item) => !Number.isFinite(item))
      ) {
        self.close();
        return;
      }
      port.postMessage({
        type: "RESULT",
        sessionId,
        sequence: request.sequence,
        output
      });
    } catch {
      self.close();
    }
  };
  port.onmessageerror = () => self.close();
  port.start();
};
Deployment and verification

Serve the artifacts from an authenticated application origin; resolve all URLs and limits from a signature-verified, versioned environment policy. The page CSP must explicitly allow the required worker/blob behavior and separately constrain egress. Never transfer DOM nodes, native bridge objects, storage objects, authentication material, or callbacks. Terminate the worker on navigation, logout, task close, timeout, sequence error, malformed clone, model failure, or policy change. Retain signed start/termination receipts plus every request/response digest and sequence, then replay the complete exchange population and negative tests. Do not claim network isolation from this worker boundary.

Do not activate without startup evidence

In startVerifiedWorker(), move the BOOTSTRAP transfer after the verified worker_started append. If that append or its exact-event readback fails, terminate the Worker, close both message ports, revoke the object URL, mark the launcher closed, and throw before any model bytes or request can be exchanged. The replacement sequence is:

try {
  await appendVerifiedEvidence({
    type: "worker_started",
    sessionId,
    workerSha256: policy.workerSha256,
    runtimeSha256: policy.runtimeSha256,
    modelSha256: policy.modelSha256,
    artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
    policySha256: policy.policySha256,
    policyGeneration: policy.generation,
    parentOrigin: location.origin
  });
} catch (error) {
  closed = true;
  channel.port1.close();
  channel.port2.close();
  worker.terminate();
  URL.revokeObjectURL(workerObjectUrl);
  throw new Error("worker startup evidence unavailable", {cause: error});
}
worker.postMessage({
  type: "BOOTSTRAP", sessionId,
  policy: {
    maxInputValues: policy.maxInputValues,
    maxOutputValues: policy.maxOutputValues
  },
  modelBytes: modelBytes.buffer
}, [channel.port2, modelBytes.buffer]);

Remove the earlier pre-evidence worker.postMessage() block when applying this replacement; it must exist exactly once.

AID-I-007-G002

Run untrusted models or their UI components in a sandboxed iframe.
Opaque-origin channel rule

With sandbox="allow-scripts" and no allow-same-origin, the frame has an opaque origin. Therefore a parent must not compare event.origin with the widget URL or use that URL as the postMessage target; that common pattern does not work for this sandbox. Bootstrap exactly one fresh MessagePort to the current frame window using *, then treat possession of that port, not origin text, as the only capability. The port exposes a small typed protocol and no sensitive parent state.

Verified parent controller
// File: sandboxed-widget-controller.js
// policy is signature-verified outside model control and supplies:
// widgetShellUrl, widgetShellSha256, maxShellBytes, maxTextBytes,
// maxReplyBytes, artifactFetchTimeoutMs, operationTimeoutMs, policySha256.
function bytesToHex(bytes) {
  return Array.from(new Uint8Array(bytes))
    .map((value) => value.toString(16).padStart(2, "0"))
    .join("");
}

function randomHex() {
  return bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
}

function requirePositivePolicyInteger(value, field) {
  if (!Number.isSafeInteger(value) || value <= 0) {
    throw new Error(field + " must be a signed positive safe integer");
  }
}

function requireNonzeroSha256(value, field) {
  if (!/^[0-9a-f]{64}$/.test(value) || /^0{64}$/.test(value)) {
    throw new Error(field + " must be a nonzero lowercase SHA-256 digest");
  }
}

async function verifiedUtf8(
  urlText,
  expectedDigest,
  limit,
  fetchTimeoutMs
) {
  requirePositivePolicyInteger(limit, "maxShellBytes");
  requirePositivePolicyInteger(fetchTimeoutMs, "artifactFetchTimeoutMs");
  requireNonzeroSha256(expectedDigest, "widgetShellSha256");
  const url = new URL(urlText, location.href);
  if (
    location.protocol !== "https:" ||
    url.protocol !== "https:" ||
    url.username ||
    url.password ||
    url.hash ||
    url.origin !== location.origin
  ) {
    throw new Error("widget shell must use the canonical application HTTPS origin");
  }
  if (typeof AbortSignal.timeout !== "function") {
    throw new Error("deadline-capable AbortSignal is unavailable");
  }
  const response = await fetch(url, {
    cache: "no-store",
    credentials: "omit",
    redirect: "error",
    signal: AbortSignal.timeout(fetchTimeoutMs)
  });
  if (!response.ok || !response.body || response.url !== url.href) {
    throw new Error("widget shell fetch failed or redirected");
  }
  const reader = response.body.getReader();
  const chunks = [];
  let total = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    total += value.byteLength;
    if (total > limit) {
      await reader.cancel();
      throw new Error("widget shell exceeds signed policy");
    }
    chunks.push(value);
  }
  const bytes = new Uint8Array(total);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  const digest = bytesToHex(await crypto.subtle.digest("SHA-256", bytes));
  if (digest !== expectedDigest) {
    throw new Error("widget shell digest mismatch");
  }
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
}

function validateText(value, maxBytes) {
  if (
    typeof value !== "string" ||
    new TextEncoder().encode(value).byteLength > maxBytes
  ) {
    throw new Error("text violates signed policy");
  }
  return value;
}

async function digestUtf8(value) {
  const digest = await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(value)
  );
  return bytesToHex(new Uint8Array(digest));
}

export async function createSandboxedWidget(container, policy, evidenceClient) {
  requirePositivePolicyInteger(policy.maxShellBytes, "maxShellBytes");
  requirePositivePolicyInteger(policy.maxTextBytes, "maxTextBytes");
  requirePositivePolicyInteger(policy.maxReplyBytes, "maxReplyBytes");
  requirePositivePolicyInteger(
    policy.artifactFetchTimeoutMs,
    "artifactFetchTimeoutMs"
  );
  requirePositivePolicyInteger(policy.operationTimeoutMs, "operationTimeoutMs");
  requirePositivePolicyInteger(policy.generation, "generation");
  if (policy.operationTimeoutMs > 2_147_483_647) {
    throw new Error("operationTimeoutMs exceeds the browser timer range");
  }
  requireNonzeroSha256(policy.policySha256, "policySha256");
  const shell = await verifiedUtf8(
    policy.widgetShellUrl,
    policy.widgetShellSha256,
    policy.maxShellBytes,
    policy.artifactFetchTimeoutMs
  );
  const frame = document.createElement("iframe");
  frame.setAttribute("sandbox", "allow-scripts");
  frame.setAttribute("referrerpolicy", "no-referrer");
  frame.setAttribute("allow", "");
  frame.setAttribute("title", "Isolated AI output");
  frame.srcdoc = shell;

  const channel = new MessageChannel();
  const channelId = randomHex();
  let sequence = 0;
  let nextResponseSequence = 1;
  let chainHeadSha256 = "0".repeat(64);
  let loadCount = 0;
  let ready = false;
  let closed = false;
  const pending = new Map();
  let resolveReady;
  let rejectReady;
  const readyPromise = new Promise((resolve, reject) => {
    resolveReady = resolve;
    rejectReady = reject;
  });
  const readyTimer = setTimeout(() => {
    rejectReady(new Error("sandbox bootstrap timed out"));
    void destroy("bootstrap_timeout");
  }, policy.operationTimeoutMs);

  const destroy = async (reason) => {
    if (closed) return;
    closed = true;
    clearTimeout(readyTimer);
    if (!ready) rejectReady(new Error("sandbox destroyed: " + reason));
    for (const waiter of pending.values()) {
      clearTimeout(waiter.timer);
      waiter.reject(new Error("sandbox destroyed: " + reason));
    }
    pending.clear();
    channel.port1.close();
    frame.remove();
    await evidenceClient.appendAndVerify({
      type: "sandboxed_widget_destroyed",
      channelId,
      reason,
      shellSha256: policy.widgetShellSha256,
      artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
      policySha256: policy.policySha256
    });
  };

  channel.port1.onmessage = async (event) => {
    const message = event.data;
    if (
      !message ||
      message.channelId !== channelId ||
      !Number.isSafeInteger(message.sequence) ||
      message.sequence !== (message.type === "READY" ? 0 : nextResponseSequence)
    ) {
      void destroy("protocol_violation");
      return;
    }
    if (message.type === "READY" && message.sequence === 0 && !ready) {
      ready = true;
      clearTimeout(readyTimer);
      resolveReady();
      return;
    }
    if (message.type !== "RENDERED") {
      void destroy("unexpected_message_type");
      return;
    }
    const waiter = pending.get(message.sequence);
    if (!waiter) {
      void destroy("unknown_or_replayed_sequence");
      return;
    }
    try {
      if (
        !/^[0-9a-f]{64}$/.test(message.textDigest) ||
        message.textDigest !== waiter.textDigest
      ) {
        throw new Error("iframe returned a non-canonical or substituted digest");
      }
      const priorChainHeadSha256 = chainHeadSha256;
      const nextChainHeadSha256 = await digestUtf8([
        priorChainHeadSha256,
        String(message.sequence),
        waiter.textDigest,
        String(waiter.textBytes),
        channelId,
        policy.policySha256,
        String(policy.generation),
        location.origin,
        frame.getAttribute("sandbox") || ""
      ].join("\u0000"));
      const receipt = await evidenceClient.appendAndVerify({
        type: "sandboxed_widget_exchange",
        channelId,
        sequence: message.sequence,
        textSha256: waiter.textDigest,
        textBytes: waiter.textBytes,
        priorChainHeadSha256,
        chainHeadSha256: nextChainHeadSha256,
        parentOrigin: location.origin,
        iframeOrigin: "opaque",
        sandbox: frame.getAttribute("sandbox"),
        policySha256: policy.policySha256,
        policyGeneration: policy.generation
      });
      if (!receipt || receipt.chainHeadSha256 !== nextChainHeadSha256) {
        throw new Error("iframe evidence readback does not bind the digest chain");
      }
      chainHeadSha256 = nextChainHeadSha256;
      nextResponseSequence += 1;
      pending.delete(message.sequence);
      clearTimeout(waiter.timer);
      waiter.resolve(message.textDigest);
    } catch (error) {
      waiter.reject(error);
      void destroy("invalid_reply");
    }
  };
  channel.port1.onmessageerror = () => {
    void destroy("message_deserialization_failure");
  };
  channel.port1.start();

  frame.addEventListener("load", () => {
    loadCount += 1;
    if (loadCount !== 1 || !frame.contentWindow) {
      void destroy("frame_navigation_or_replacement");
      return;
    }
    // The target must be "*" because the sandboxed frame has an opaque origin.
    // Safety comes from the fresh port and intentionally non-sensitive protocol.
    frame.contentWindow.postMessage(
      { type: "BOOTSTRAP", channelId },
      "*",
      [channel.port2]
    );
  });

  container.replaceChildren(frame);
  await readyPromise;
  await evidenceClient.appendAndVerify({
    type: "sandboxed_widget_created",
    channelId,
    sandbox: frame.getAttribute("sandbox"),
    shellSha256: policy.widgetShellSha256,
    artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
    policySha256: policy.policySha256
  });

  return {
    channelId,
    async renderText(untrustedText) {
      if (closed || !ready) throw new Error("sandbox is not ready");
      const text = validateText(untrustedText, policy.maxTextBytes);
      const textBytes = new TextEncoder().encode(text).byteLength;
      const textDigest = await digestUtf8(text);
      const requestSequence = ++sequence;
      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          pending.delete(requestSequence);
          reject(new Error("sandbox operation timed out"));
          void destroy("operation_timeout");
        }, policy.operationTimeoutMs);
        pending.set(requestSequence, {
          resolve, reject, timer, textDigest, textBytes
        });
        channel.port1.postMessage({
          type: "RENDER_TEXT",
          channelId,
          sequence: requestSequence,
          text
        });
      });
    },
    close: () => destroy("controller_close")
  };
}
Digest-pinned frame shell
<!doctype html>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'">
<style>html,body{margin:0}#output{white-space:pre-wrap;overflow-wrap:anywhere}</style>
<div id="output" aria-live="polite"></div>
<script>
"use strict";
let capabilityPort = null;
let channelId = null;
let lastSequence = 0;
const output = document.getElementById("output");

function digestHex(text) {
  return crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(text)
  ).then((buffer) => Array.from(new Uint8Array(buffer))
    .map((value) => value.toString(16).padStart(2, "0"))
    .join(""));
}

function bootstrap(event) {
  const message = event.data;
  if (
    capabilityPort ||
    event.source !== parent ||
    !message ||
    message.type !== "BOOTSTRAP" ||
    typeof message.channelId !== "string" ||
    event.ports.length !== 1
  ) {
    return;
  }
  removeEventListener("message", bootstrap);
  channelId = message.channelId;
  capabilityPort = event.ports[0];
  capabilityPort.onmessage = async (requestEvent) => {
    const request = requestEvent.data;
    if (
      !request ||
      request.type !== "RENDER_TEXT" ||
      request.channelId !== channelId ||
      !Number.isSafeInteger(request.sequence) ||
      request.sequence !== lastSequence + 1 ||
      typeof request.text !== "string"
    ) {
      capabilityPort.close();
      location.replace("about:blank");
      return;
    }
    lastSequence = request.sequence;
    // Never parse model output as HTML or script.
    output.textContent = request.text;
    capabilityPort.postMessage({
      type: "RENDERED",
      channelId,
      sequence: request.sequence,
      textDigest: await digestHex(request.text)
    });
  };
  capabilityPort.onmessageerror = () => {
    capabilityPort.close();
    location.replace("about:blank");
  };
  capabilityPort.start();
  capabilityPort.postMessage({ type: "READY", channelId, sequence: 0 });
}
addEventListener("message", bootstrap);
</script>
Deployment and evidence

The shell is trusted code whose bytes are pinned by signed policy; only the rendered model text is untrusted. Keep allow-same-origin, forms, popups, downloads, storage access, and top-navigation absent. Enforce an equivalent or stronger CSP as an HTTP header for URL-based shells; the meta policy shown here is for the self-contained srcdoc shell and cannot provide frame-ancestors. Do not send credentials, tokens, DOM/native handles, or privileged commands through the channel. Destroy the frame on any later load, task close, logout, timeout, malformed message, policy change, or parent navigation. Retain shell/policy digests, effective sandbox/CSP capture, ordered message digests, lifecycle receipts, and negative-test results. This step does not claim that a sandboxed frame is a complete egress boundary.

Verify safely

inspect effective sandbox flags and replay traffic while testing opaque origin, frame replacement, forged messages, navigation, popup, form, and storage.

Tear down on creation-evidence failure

The controller is not usable until the sandboxed_widget_created append is verified. Wrap that append in try/catch; on failure set closed, clear the readiness timer, reject every pending request, close both ports, and remove the frame before rethrowing. Do not call the normal evidence-writing destroy() as the only cleanup because the same sink may be unavailable.

try {
  await evidenceClient.appendAndVerify({
    type: "sandboxed_widget_created",
    channelId,
    sandbox: frame.getAttribute("sandbox"),
    shellSha256: policy.widgetShellSha256,
    artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
    policySha256: policy.policySha256,
    policyGeneration: policy.generation
  });
} catch (error) {
  closed = true;
  clearTimeout(readyTimer);
  for (const waiter of pending.values()) {
    clearTimeout(waiter.timer);
    waiter.reject(new Error("sandbox creation evidence unavailable"));
  }
  pending.clear();
  channel.port1.close();
  try { channel.port2.close(); } catch {}
  frame.remove();
  throw error;
}

Replace the earlier unguarded creation-evidence append with this block. No renderText() capability is returned until it succeeds.

AID-I-007-G003

Leverage WebAssembly (WASM) runtimes for a capabilities-based sandbox.
Security boundary and limits

WebAssembly receives no browser, network, filesystem, clipboard, camera, DOM, or native capability unless the host imports one. Verify the exact module and import population before instantiation. This is a capability boundary, not a complete resource-exhaustion boundary: execute the module inside the verified Worker from #1 and terminate that worker on signed-policy timeout or resource breach. Do not claim instruction, CPU, or memory isolation that the browser API cannot independently prove.

Verified no-import module adapter
// File: wasm/verified-module.js
// Signed policy includes maxModuleBytes, artifactFetchTimeoutMs,
// maxObservedMemoryBytes, maxEntryArguments, and exact module bindings.
function hex(buffer) {
  return Array.from(new Uint8Array(buffer))
    .map((value) => value.toString(16).padStart(2, "0"))
    .join("");
}

function requirePositivePolicyInteger(value, field) {
  if (!Number.isSafeInteger(value) || value <= 0) {
    throw new Error(field + " must be a signed positive safe integer");
  }
}

function requireNonzeroSha256(value, field) {
  if (!/^[0-9a-f]{64}$/.test(value) || /^0{64}$/.test(value)) {
    throw new Error(field + " must be a nonzero lowercase SHA-256 digest");
  }
}

async function fetchBounded(urlText, maxBytes, fetchTimeoutMs) {
  requirePositivePolicyInteger(maxBytes, "maxModuleBytes");
  requirePositivePolicyInteger(fetchTimeoutMs, "artifactFetchTimeoutMs");
  const url = new URL(urlText, self.location.href);
  if (
    self.location.protocol !== "https:" ||
    url.protocol !== "https:" ||
    url.username ||
    url.password ||
    url.hash ||
    url.origin !== self.location.origin
  ) {
    throw new Error("WASM artifact must use the canonical worker HTTPS origin");
  }
  if (typeof AbortSignal.timeout !== "function") {
    throw new Error("deadline-capable AbortSignal is unavailable");
  }
  const response = await fetch(url, {
    cache: "no-store",
    credentials: "omit",
    redirect: "error",
    signal: AbortSignal.timeout(fetchTimeoutMs)
  });
  if (!response.ok || !response.body || response.url !== url.href) {
    throw new Error("WASM fetch failed or redirected");
  }
  const reader = response.body.getReader();
  const chunks = [];
  let size = 0;
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const bytes = new Uint8Array(value);
      size += bytes.byteLength;
      if (size > maxBytes) {
        await reader.cancel();
        throw new Error("WASM artifact exceeds signed policy");
      }
      chunks.push(bytes);
    }
  } finally {
    reader.releaseLock();
  }
  const artifact = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) {
    artifact.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return artifact;
}

function exactDescriptors(items) {
  return items
    .map((item) => ({
      module: item.module,
      name: item.name,
      kind: item.kind
    }))
    .sort((left, right) =>
      JSON.stringify(left).localeCompare(JSON.stringify(right))
    );
}

export async function instantiateVerifiedWasm(policy, evidenceClient) {
  requirePositivePolicyInteger(policy.maxModuleBytes, "maxModuleBytes");
  requirePositivePolicyInteger(
    policy.artifactFetchTimeoutMs,
    "artifactFetchTimeoutMs"
  );
  requirePositivePolicyInteger(
    policy.maxObservedMemoryBytes,
    "maxObservedMemoryBytes"
  );
  requirePositivePolicyInteger(
    policy.maxEntryArguments,
    "maxEntryArguments"
  );
  requireNonzeroSha256(policy.moduleSha256, "moduleSha256");
  requireNonzeroSha256(policy.policySha256, "policySha256");
  const bytes = await fetchBounded(
    policy.moduleUrl,
    policy.maxModuleBytes,
    policy.artifactFetchTimeoutMs
  );
  const observedDigest = hex(
    await crypto.subtle.digest("SHA-256", bytes)
  );
  if (observedDigest !== policy.moduleSha256) {
    throw new Error("WASM artifact digest mismatch");
  }
  if (!WebAssembly.validate(bytes)) {
    throw new Error("WASM binary validation failed");
  }

  const module = new WebAssembly.Module(bytes);
  const imports = exactDescriptors(WebAssembly.Module.imports(module));
  const exports = exactDescriptors(WebAssembly.Module.exports(module));
  if (
    JSON.stringify(imports) !==
      JSON.stringify(exactDescriptors(policy.allowedImports)) ||
    JSON.stringify(exports) !==
      JSON.stringify(exactDescriptors(policy.expectedExports))
  ) {
    throw new Error("WASM import/export population differs from signed policy");
  }
  if (imports.length !== 0) {
    throw new Error("this no-capability adapter refuses all host imports");
  }

  const instance = await WebAssembly.instantiate(module, {});
  const entry = instance.exports[policy.entryExport];
  if (typeof entry !== "function") {
    throw new Error("signed-policy entry export is not callable");
  }
  const args = policy.entryArguments;
  if (
    !Array.isArray(args) ||
    args.length > policy.maxEntryArguments ||
    args.some((value) =>
      typeof value !== "number" || !Number.isFinite(value)
    )
  ) {
    throw new Error("entry arguments violate signed policy");
  }
  const result = entry.apply(undefined, args);
  if (typeof result !== "number" || !Number.isFinite(result)) {
    throw new Error("WASM output is not one finite numeric value");
  }
  if (
    instance.exports.memory instanceof WebAssembly.Memory &&
    instance.exports.memory.buffer.byteLength > policy.maxObservedMemoryBytes
  ) {
    throw new Error("observed WASM memory exceeds signed policy");
  }

  const event = {
    type: "wasm_execution",
    moduleSha256: observedDigest,
    policySha256: policy.policySha256,
    artifactFetchTimeoutMs: policy.artifactFetchTimeoutMs,
    runtimeVersion: navigator.userAgent,
    imports,
    exports,
    entryExport: policy.entryExport,
    argumentCount: args.length,
    maxEntryArguments: policy.maxEntryArguments,
    maxObservedMemoryBytes: policy.maxObservedMemoryBytes,
    outputSha256: hex(
      await crypto.subtle.digest(
        "SHA-256", new TextEncoder().encode(String(result))
      )
    ),
    observedMemoryBytes:
      instance.exports.memory instanceof WebAssembly.Memory
        ? instance.exports.memory.buffer.byteLength
        : 0,
    outcome: "PASS"
  };
  const receipt = await evidenceClient.append(event);
  const readback = await evidenceClient.read(receipt.receiptId);
  if (readback.eventSha256 !== receipt.eventSha256) {
    throw new Error("WASM evidence readback mismatch");
  }
  return { result, evidenceReceiptId: receipt.receiptId };
}
Capability-bearing variants

If the module requires imports, create a different reviewed adapter for one named capability set. Compare the complete WebAssembly.Module.imports() list with signed policy and provide only bounded functions that validate pointer/range, length, encoding, identity, rate state, destination, and result before reading or writing module memory. Never pass raw fetch, filesystem/native bridges, credentials, or unrestricted logging. Pin browser/runtime and module versions, run inside a fresh Worker, and retain module/policy digests, complete import/export sets, host-call population, resource observations, output digest, termination event, and independent readback. Exercise unexpected imports/exports, artifact redirect/substitution, malformed binary, oversized module, memory growth, trap, timeout, invalid pointer, host-call abuse, and evidence outage.

AID-I-007-G004

Utilize Content Security Policy (CSP) to restrict model data exfiltration and script execution.
Concept:

Content Security Policy (CSP) lets you define which network endpoints scripts in this page are allowed to talk to (connect-src), which scripts can run (script-src), and more. A strict CSP makes it much harder for a compromised in-browser model to exfiltrate sensitive data to an attacker-controlled domain, or to inject arbitrary remote scripts. Vendor and checksum-verify the AI runtime in the build; do not add a public CDN to the runtime trust boundary.

Example CSP header

Header format: Send this as one HTTP header field value. Do not copy folded or multiline formatting into the actual response.

Content-Security-Policy: default-src 'none'; script-src 'sha256-BASE64_BUILD_SCRIPT_DIGEST'; style-src 'sha256-BASE64_BUILD_STYLE_DIGEST'; worker-src blob:; connect-src https://api.my-trusted-domain.com; img-src data:; frame-src 'none'; child-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; 

This policy says:

Meta tag fallback (if you cannot set headers)

Prefer the HTTP header when possible. A meta-delivered policy cannot enforce directives such as frame-ancestors, so do not treat it as equivalent for clickjacking protection.

<head>
  <meta http-equiv="Content-Security-Policy"
        content="default-src 'none'; connect-src https://api.my-trusted-domain.com; script-src 'sha256-BASE64_BUILD_SCRIPT_DIGEST'; worker-src blob:; object-src 'none'; base-uri 'none'; form-action 'none';">
</head>

Detection tip: CSP can also emit violation reports (via report-to / report-uri). Treat repeated CSP violations from a given session as a potential sign of a compromised model trying to leak data.

Deployment verification: Generate the concrete script/style hashes and exact API origins from the signed build manifest; never ship the placeholders above. Fetch the deployed document through the same edge/CDN route users receive, read back the response header, and run browser probes that attempt unlisted same-origin and cross-origin fetch, script, worker, frame, form, object, base, and navigation paths. Missing response-header authority is an unresolved provider-evidence gap, not PASS; a meta-only policy cannot satisfy the response-header or frame-ancestors claim. Monitor CSP violation reports for attempted exfiltration or unexpected loads.

CSP does not provide a supported navigate-to directive. Keep model code in the DOM-less worker, and render untrusted output through AID-H-006.002. If a preview uses an iframe, omit allow-top-navigation, allow-popups and allow-forms from its sandbox; only the trusted parent navigation broker may open an approved destination. Verify blocked top-level navigation, popup and form submission through the actual worker/iframe boundary separately from CSP resource-fetch tests.

AID-I-007-G005

Enforce a minimal, allowlisted native bridge between the AI runtime and device/system capabilities (mobile, Electron, hybrid apps).
Bridge boundary

The renderer receives named functions, never ipcRenderer, arbitrary channel names, Node globals, callbacks containing the IPC event, filesystem paths, credentials, or a generic native-call function. Keep contextIsolation, renderer sandboxing, and nodeIntegration: false; validate the sender frame for every IPC call because child frames can also send IPC. A separately authenticated native broker applies signed capability policy, validation, redaction, rate state, and append-only evidence. This reference is Electron-specific; mobile/hybrid platforms need an equivalent broker and effective-permission readback.

Sandboxed window and main-process handler
// File: main.cjs
const path = require("node:path");
const { createHash } = require("node:crypto");
const { app, BrowserWindow, ipcMain, session } = require("electron");

// Injected at the composition root after signature verification.
const signedPolicy = require("./verified-native-bridge-policy.cjs");
const nativeBroker = require("./native-broker-client.cjs");
const evidence = require("./evidence-client.cjs");

app.enableSandbox();
const registeredWindows = new Map();

function canonicalFrameUrlSha256(value) {
  const url = new URL(value);
  if (
    url.protocol !== "app:" ||
    url.host !== "bundle" ||
    url.pathname !== "/index.html" ||
    url.username || url.password || url.search || url.hash
  ) {
    throw new Error("main-frame URL is outside the signed application origin");
  }
  return "sha256:" + createHash("sha256").update(url.href, "utf8").digest("hex");
}

function exactTrustedFrame(frame, webContentsId) {
  if (!frame || frame.top !== frame) return false;
  const url = new URL(frame.url);
  return (
    registeredWindows.get(webContentsId) === frame.url &&
    url.protocol === "app:" &&
    url.host === "bundle" &&
    url.pathname === "/index.html" &&
    url.username === "" &&
    url.password === "" &&
    url.search === "" &&
    url.hash === ""
  );
}

function plainObject(value) {
  return (
    value !== null &&
    typeof value === "object" &&
    !Array.isArray(value) &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}

function validateTelemetryRequest(value) {
  if (
    !plainObject(value) ||
    !/^[0-9a-f]{32}$/.test(value.requestId) ||
    typeof value.text !== "string" ||
    Buffer.byteLength(value.text, "utf8") >
      signedPolicy.maxTelemetryBytes ||
    Object.keys(value).sort().join(",") !== "requestId,text"
  ) {
    throw new Error("native bridge request violates signed policy");
  }
  return { requestId: value.requestId, text: value.text };
}

async function createWindow() {
  const window = new BrowserWindow({
    show: false,
    webPreferences: {
      preload: path.join(app.getAppPath(), "preload.cjs"),
      contextIsolation: true,
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      sandbox: true,
      webSecurity: true,
      allowRunningInsecureContent: false,
      experimentalFeatures: false
    }
  });

  const webContentsId = window.webContents.id;
  window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
  window.webContents.on("will-navigate", (event, target) => {
    if (target !== "app://bundle/index.html") event.preventDefault();
  });
  window.webContents.on("will-attach-webview", (event) => {
    event.preventDefault();
  });
  window.webContents.on("destroyed", () => {
    registeredWindows.delete(webContentsId);
  });

  await window.loadURL("app://bundle/index.html");
  const effectiveUrl = window.webContents.mainFrame.url;
  if (
    effectiveUrl !== "app://bundle/index.html" ||
    canonicalFrameUrlSha256(effectiveUrl) !== signedPolicy.mainFrameUrlSha256
  ) {
    window.destroy();
    throw new Error("packaged application URL or signed digest mismatch");
  }
  registeredWindows.set(webContentsId, effectiveUrl);
  window.show();
  return window;
}

session.defaultSession.setPermissionCheckHandler(() => false);
session.defaultSession.setPermissionRequestHandler(
  (_webContents, _permission, callback) => callback(false)
);

ipcMain.handle("secure-bridge:telemetry", async (event, raw) => {
  if (!exactTrustedFrame(event.senderFrame, event.sender.id)) {
    throw new Error("untrusted IPC sender");
  }
  const request = validateTelemetryRequest(raw);
  const effectiveFrameUrlSha256 = canonicalFrameUrlSha256(event.senderFrame.url);
  if (effectiveFrameUrlSha256 !== signedPolicy.mainFrameUrlSha256) {
    throw new Error("effective main-frame digest differs from signed policy");
  }
  const principal = {
    appId: signedPolicy.appId,
    webContentsId: event.sender.id,
    frameUrlSha256: effectiveFrameUrlSha256
  };
  const brokerReceipt = await nativeBroker.callAndVerify({
    capability: "SEND_REDACTED_TELEMETRY",
    principal,
    request,
    policySha256: signedPolicy.policySha256,
    idempotencyKey: request.requestId
  });
  if (
    brokerReceipt.capability !== "SEND_REDACTED_TELEMETRY" ||
    brokerReceipt.requestId !== request.requestId ||
    brokerReceipt.policySha256 !== signedPolicy.policySha256
  ) {
    throw new Error("native broker receipt binding mismatch");
  }
  const eventReceipt = await evidence.appendAndVerify({
    control: "AID-I-007",
    guidanceRef: "AID-I-007-G005",
    capability: brokerReceipt.capability,
    requestId: request.requestId,
    principal,
    inputSha256: brokerReceipt.inputSha256,
    redactedOutputSha256: brokerReceipt.redactedOutputSha256,
    brokerReceiptSha256: brokerReceipt.receiptSha256,
    policySha256: signedPolicy.policySha256,
    outcome: "PASS"
  });
  return Object.freeze({
    accepted: true,
    requestId: request.requestId,
    evidenceReceiptId: eventReceipt.receiptId
  });
});

app.whenReady().then(createWindow);
app.on("window-all-closed", () => app.quit());
Minimal preload API
// File: preload.cjs
const { contextBridge, ipcRenderer } = require("electron");

function validateRendererInput(value) {
  if (
    !value ||
    typeof value !== "object" ||
    Array.isArray(value) ||
    !/^[0-9a-f]{32}$/.test(value.requestId) ||
    typeof value.text !== "string"
  ) {
    throw new TypeError("invalid telemetry request");
  }
  return { requestId: value.requestId, text: value.text };
}

contextBridge.exposeInMainWorld("secureBridge", Object.freeze({
  sendRedactedTelemetry(input) {
    return ipcRenderer.invoke(
      "secure-bridge:telemetry", validateRendererInput(input)
    );
  }
}));
Deployment and verification

Register the packaged app: protocol as standard/secure before use, sign and verify the application bundle and preload/main files, and generate verified-native-bridge-policy.cjs from a signed versioned policy at deployment. Use Electron fuses and package inspection to prove Node integration, CLI inspection, and unsafe debugging features are not re-enabled. The native broker uses mTLS workload identity, accepts only enumerated capabilities, independently resolves any native resource, enforces policy-derived size/rate/destination limits, strips secrets, and returns signed receipts; it never trusts renderer-supplied identity or path. Preserve effective BrowserWindow preferences, permission handlers, navigation/window-open probes, bundle/bridge/policy digests, complete IPC population, broker receipts, and independent evidence readback. Test subframe/sender spoofing, arbitrary channel, prototype and oversized input, navigation, popup/webview, permission requests, broker/evidence outage, replay, and packaged setting drift. Keep Electron current through the product patch process; PASS applies only to the measured packaged build and capability set.

Account for pre-broker denials

Every invocation of secure-bridge:telemetry belongs to the IPC population, including attempts rejected before nativeBroker.callAndVerify(). Add a main-process monotonic sequence and record a bounded observed sender/request shape; never serialize attacker-controlled payloads merely for logging.

let bridgeAttemptSequence = 0;

async function denyBeforeBroker(event, raw, reason) {
  const sequence = ++bridgeAttemptSequence;
  const shape = {
    valueType: raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw,
    keys: plainObject(raw) ? Object.keys(raw).sort().slice(0, 16) : [],
    keyCount: plainObject(raw) ? Object.keys(raw).length : 0
  };
  const record = {
    control: "AID-I-007",
    guidanceRef: "AID-I-007-G005",
    outcome: "DENY",
    reason,
    attemptSequence: sequence,
    brokerInvoked: false,
    webContentsId: event.sender.id,
    observedFrameUrl: String(event.senderFrame?.url || ""),
    requestShape: shape,
    policySha256: signedPolicy.policySha256
  };
  const receipt = await evidence.appendAndVerify(record);
  if (!receipt || receipt.attemptSequence !== sequence) {
    throw new Error("bridge denial evidence unavailable");
  }
  throw new Error("native bridge request denied");
}

// Use before any broker call:
async function validateBeforeBroker(event, raw) {
  if (!exactTrustedFrame(event.senderFrame, event.sender.id)) {
    return denyBeforeBroker(event, raw, "UNTRUSTED_SENDER");
  }
  try {
    return validateTelemetryRequest(raw);
  } catch {
    return denyBeforeBroker(event, raw, "INVALID_REQUEST_SCHEMA");
  }
}

// Inside the IPC handler, before nativeBroker.callAndVerify():
const request = await validateBeforeBroker(event, raw);

Replace the handler's corresponding early throws with these calls. Reconcile attemptSequence across allowed and denied terminal records; an evidence outage still denies the bridge and leaves the population unresolved.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-I-007: Client-Side AI Execution Isolation | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version