← Files Google DriveARCHIVED FILE

skills/google-docs/host/docs-trusted-read-file-bridge.mjs

22.5 KB · Oct 3, 2026 · 06:02 UTC

↓ Download file

/*
 * Dependency-free file bridge for Google Docs trusted reads.
 *
 * The bridge invokes authenticated connector reads inside the code-mode isolate,
 * persists rich responses through a private no-echo receiver, and returns only
 * compact control awareness plus immutable file metadata.
 */

const BRIDGE_VERSION = "3.6";
const DEFAULT_MAX_READ_BYTES = 4 * 1024 * 1024;
const DEFAULT_MAX_WRITE_BYTES = 32 * 1024 * 1024;
const DEFAULT_MAX_WRITE_STDIN_CHARS = 512 * 1024;
const MAX_WRITE_STDIN_CHARS = 1024 * 1024;
const DEFAULT_RECEIVER_POLL_ATTEMPTS = 4;
const WRITE_STDIN_RECORD_SEPARATOR = "\u0003";
const RECEIVER_READY_MARKER = "DOCS_BRIDGE_READY";
const RECEIVER_COMMITTED_MARKER = "DOCS_BRIDGE_COMMITTED";
const TRUSTED_SOURCE_SHA256 = new Map([
  ["/host/docs-trusted-read-wrapper.mjs", "c7c1affe5fd3e37924f005c27aa0d6ffbc10e1d2d7ca8fb55d8f38f288ca2ead"],
  ["/runtime/docs-read-normalizer.mjs", "324de06dedc0476c91f7f3d2fe4b15c4cb992ace2afb9e57df498a994020878a"],
]);

export class DocsTrustedReadBridgeError extends Error {
  constructor(message, { path = null, operation = null, cause = null } = {}) {
    super(redactText(message));
    this.name = "DocsTrustedReadBridgeError";
    this.path = path;
    this.operation = operation;
    this.cause = cause;
  }
}

function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function assert(condition, message, details = {}) {
  if (!condition) throw new DocsTrustedReadBridgeError(message, details);
}

function redactText(value) {
  return String(value ?? "")
    .replace(/https?:\/\/[^\s"'<>]+/gi, (url) => /(?:x-goog-|x-amz-|access_token=|signature=)/i.test(url) ? "[EPHEMERAL_URL_REDACTED]" : url)
    .replace(/\b(authorization\s*:\s*bearer|bearer)\s+[A-Za-z0-9._~+\/=:-]+/gi, "$1 [REDACTED]")
    .replace(/\b(x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["']?[^\s"',;}\]]*/gi, "$1=[REDACTED]");
}

function normalizeAbsolutePath(value, label) {
  assert(typeof value === "string" && value.startsWith("/"), `${label} must be an absolute path`, { path: value, operation: "validate-path" });
  assert(!/[\0\r\n]/.test(value), `${label} contains an invalid path character`, { path: value, operation: "validate-path" });
  const parts = [];
  for (const part of value.split("/")) {
    if (!part || part === ".") continue;
    if (part === "..") parts.pop();
    else parts.push(part);
  }
  return `/${parts.join("/")}`;
}

function assertWithinRoot(path, root, label) {
  const normalizedPath = normalizeAbsolutePath(path, label);
  const normalizedRoot = normalizeAbsolutePath(root, `${label} root`);
  assert(
    normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`),
    `${label} must stay within ${normalizedRoot}`,
    { path: normalizedPath, operation: "validate-path" },
  );
  return normalizedPath;
}

function dirname(path) {
  const index = path.lastIndexOf("/");
  return index <= 0 ? "/" : path.slice(0, index);
}

function joinPath(root, child) {
  assert(typeof child === "string" && child.length > 0 && !child.includes("/"), "Output filename must be one path segment", { path: child, operation: "join-path" });
  return `${root.replace(/\/+$/, "")}/${child}`;
}

function shellQuote(value) {
  return `'${String(value).replace(/'/g, `'"'"'`)}'`;
}

function utf8Bytes(text) {
  if (typeof TextEncoder === "function") return new TextEncoder().encode(text);
  const bytes = [];
  for (let index = 0; index < text.length; index += 1) {
    let codePoint = text.charCodeAt(index);
    if (codePoint >= 0xd800 && codePoint <= 0xdbff && index + 1 < text.length) {
      const next = text.charCodeAt(index + 1);
      if (next >= 0xdc00 && next <= 0xdfff) {
        codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00);
        index += 1;
      }
    }
    if (codePoint <= 0x7f) bytes.push(codePoint);
    else if (codePoint <= 0x7ff) bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
    else if (codePoint <= 0xffff) bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
    else bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
  }
  return Uint8Array.from(bytes);
}

function base64Text(text) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  const bytes = utf8Bytes(text);
  const chunks = [];
  let chunk = "";
  for (let index = 0; index < bytes.length; index += 3) {
    const first = bytes[index];
    const hasSecond = index + 1 < bytes.length;
    const hasThird = index + 2 < bytes.length;
    const second = hasSecond ? bytes[index + 1] : 0;
    const third = hasThird ? bytes[index + 2] : 0;
    chunk += alphabet[first >> 2];
    chunk += alphabet[((first & 0x03) << 4) | (second >> 4)];
    chunk += hasSecond ? alphabet[((second & 0x0f) << 2) | (third >> 6)] : "=";
    chunk += hasThird ? alphabet[third & 0x3f] : "=";
    if (chunk.length >= 8192) {
      chunks.push(chunk);
      chunk = "";
    }
  }
  if (chunk) chunks.push(chunk);
  return chunks.join("");
}

const SHA256_K = [
  0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
  0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
  0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
  0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
  0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
  0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
  0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
];

export function sha256Text(text) {
  const bytes = utf8Bytes(text);
  const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
  const padded = new Uint8Array(paddedLength);
  padded.set(bytes);
  padded[bytes.length] = 0x80;
  const bitLength = BigInt(bytes.length) * 8n;
  const view = new DataView(padded.buffer);
  view.setUint32(paddedLength - 8, Number((bitLength >> 32n) & 0xffffffffn));
  view.setUint32(paddedLength - 4, Number(bitLength & 0xffffffffn));
  const state = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
  const words = new Uint32Array(64);
  const rotateRight = (input, shift) => (input >>> shift) | (input << (32 - shift));
  for (let offset = 0; offset < paddedLength; offset += 64) {
    for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4);
    for (let index = 16; index < 64; index += 1) {
      const s0 = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ (words[index - 15] >>> 3);
      const s1 = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ (words[index - 2] >>> 10);
      words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
    }
    let [a, b, c, d, e, f, g, h] = state;
    for (let index = 0; index < 64; index += 1) {
      const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
      const choice = (e & f) ^ (~e & g);
      const temp1 = (h + s1 + choice + SHA256_K[index] + words[index]) >>> 0;
      const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
      const majority = (a & b) ^ (a & c) ^ (b & c);
      const temp2 = (s0 + majority) >>> 0;
      h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0;
    }
    state[0] = (state[0] + a) >>> 0; state[1] = (state[1] + b) >>> 0;
    state[2] = (state[2] + c) >>> 0; state[3] = (state[3] + d) >>> 0;
    state[4] = (state[4] + e) >>> 0; state[5] = (state[5] + f) >>> 0;
    state[6] = (state[6] + g) >>> 0; state[7] = (state[7] + h) >>> 0;
  }
  return [...state].map((value) => value.toString(16).padStart(8, "0")).join("");
}

function fileArtifact(path, text) {
  const normalized = text.endsWith("\n") ? text : `${text}\n`;
  return {
    path,
    text: normalized,
    metadata: { path, bytes: utf8Bytes(normalized).length, sha256: sha256Text(normalized) },
  };
}

function jsonArtifact(path, value) {
  return fileArtifact(path, JSON.stringify(value, null, 2));
}

function compactError(error) {
  return {
    name: error?.name ?? "Error",
    message: redactText(error?.message ?? String(error)),
    operation: error?.operation ?? null,
  };
}

function moduleFactory(source, exports, label) {
  assert(typeof source === "string" && source.length > 0, `${label} source is empty`, { operation: "evaluate-module" });
  assert(!/^\s*import\s/m.test(source), `${label} must remain dependency-free`, { operation: "evaluate-module" });
  try {
    const executableSource = source
      .replace(/^\s*export\s+\{[^}]*\};?\s*$/gm, "")
      .replace(/^\s*export\s+/gm, "");
    return new Function(`${executableSource}\nreturn { ${exports.join(", ")} };`)();
  } catch (error) {
    throw new DocsTrustedReadBridgeError(`Could not evaluate ${label}: ${error?.message ?? String(error)}`, { operation: "evaluate-module", cause: error });
  }
}

async function loadTrustedModule(fileIO, path, exports, label) {
  const trusted = [...TRUSTED_SOURCE_SHA256].find(([suffix]) => path.endsWith(suffix));
  assert(trusted, `${label} path is not trusted`, { path, operation: "evaluate-module" });
  const source = await fileIO.readText(path);
  assert(sha256Text(source) === trusted[1], `${label} source hash does not match the Google Docs trust catalog`, { path, operation: "evaluate-module" });
  return moduleFactory(source, exports, label);
}

export function createToolBackedFileIO({
  tools,
  workspaceRoot,
  skillRoot,
  receiverPath,
  maxReadBytes = DEFAULT_MAX_READ_BYTES,
  maxWriteBytes = DEFAULT_MAX_WRITE_BYTES,
  maxWriteStdinChars = DEFAULT_MAX_WRITE_STDIN_CHARS,
} = {}) {
  assert(isRecord(tools), "tools is required", { operation: "create-file-io" });
  assert(typeof tools.exec_command === "function", "tools.exec_command is required", { operation: "create-file-io" });
  assert(typeof tools.write_stdin === "function", "tools.write_stdin is required", { operation: "create-file-io" });
  assert(Number.isInteger(maxWriteStdinChars) && maxWriteStdinChars >= 128 && maxWriteStdinChars <= MAX_WRITE_STDIN_CHARS, "maxWriteStdinChars is invalid", { operation: "create-file-io" });
  const workspace = normalizeAbsolutePath(workspaceRoot, "workspaceRoot");
  const skill = normalizeAbsolutePath(skillRoot, "skillRoot");
  const receiver = assertWithinRoot(receiverPath, skill, "receiverPath");
  const preparedParents = new Set();

  const validateReadable = (path) => {
    const normalized = normalizeAbsolutePath(path, "read path");
    assert(
      normalized === workspace || normalized.startsWith(`${workspace}/`) || normalized === skill || normalized.startsWith(`${skill}/`),
      "Read path is outside the allowed roots",
      { path: normalized, operation: "read-file" },
    );
    return normalized;
  };
  const validateWritable = (path) => assertWithinRoot(path, workspace, "write path");
  const run = (cmd, maxOutputTokens = 2000) => tools.exec_command({
    cmd,
    workdir: workspace,
    login: false,
    yield_time_ms: 30000,
    max_output_tokens: maxOutputTokens,
  });

  const waitForMarker = async ({ initial, sessionId, marker, operation }) => {
    let result = initial;
    let output = String(result?.output ?? "");
    for (let attempt = 0; attempt <= DEFAULT_RECEIVER_POLL_ATTEMPTS; attempt += 1) {
      if (output.includes(marker)) return { result, output };
      if (result?.exit_code !== null && result?.exit_code !== undefined) break;
      result = await tools.write_stdin({ session_id: sessionId, chars: "", yield_time_ms: 1000, max_output_tokens: 1000 });
      output += String(result?.output ?? "");
    }
    throw new DocsTrustedReadBridgeError(`Streaming receiver did not report ${marker}`, { operation });
  };

  const streamText = async (path, text) => {
    const absolute = validateWritable(path);
    const normalized = text.endsWith("\n") ? text : `${text}\n`;
    const bytes = utf8Bytes(normalized).length;
    assert(bytes <= maxWriteBytes, `${absolute} exceeds the bridge writer limit`, { path: absolute, operation: "write-file" });
    const encoded = base64Text(normalized);
    const expectedSha256 = sha256Text(normalized);
    const temporary = `${absolute}.docs-bridge-${expectedSha256.slice(0, 16)}.tmp`;
    const parent = dirname(absolute);
    if (!preparedParents.has(parent)) {
      const mkdir = await run(`/bin/mkdir -p ${shellQuote(parent)}`, 1000);
      assert(mkdir.exit_code === 0, `Could not create bridge output directory: ${mkdir.output ?? ""}`, { path: parent, operation: "write-file" });
      preparedParents.add(parent);
    }
    const start = await tools.exec_command({
      cmd: `/usr/bin/perl ${shellQuote(receiver)} ${shellQuote(absolute)} ${shellQuote(String(bytes))} ${shellQuote(expectedSha256)} ${shellQuote(temporary)}`,
      workdir: workspace,
      login: false,
      tty: true,
      yield_time_ms: 1000,
      max_output_tokens: 1000,
    });
    if (String(start?.output ?? "").includes("DOCS_BRIDGE_ERROR destination-exists")) {
      throw new DocsTrustedReadBridgeError("Refusing to overwrite a bridge artifact", { path: absolute, operation: "write-file" });
    }
    const sessionId = start?.session_id;
    assert(Number.isInteger(sessionId), "Could not start the no-echo receiver", { path: absolute, operation: "write-file" });
    try {
      await waitForMarker({ initial: start, sessionId, marker: RECEIVER_READY_MARKER, operation: "write-file" });
      const chunkChars = Math.floor((maxWriteStdinChars - 64) / 4) * 4;
      let sequence = 0;
      for (let offset = 0; offset < encoded.length; offset += chunkChars) {
        const frame = `D\t${sequence}\t${encoded.slice(offset, offset + chunkChars)}${WRITE_STDIN_RECORD_SEPARATOR}`;
        const result = await tools.write_stdin({ session_id: sessionId, chars: frame, yield_time_ms: 250, max_output_tokens: 1000 });
        assert(result?.exit_code === null || result?.exit_code === undefined, "Receiver exited before commit", { path: absolute, operation: "write-file" });
        sequence += 1;
      }
      const commit = await tools.write_stdin({ session_id: sessionId, chars: `C${WRITE_STDIN_RECORD_SEPARATOR}`, yield_time_ms: 30000, max_output_tokens: 1000 });
      const completed = await waitForMarker({ initial: commit, sessionId, marker: RECEIVER_COMMITTED_MARKER, operation: "write-file" });
      const receipt = new RegExp(`${RECEIVER_COMMITTED_MARKER}\\s+(\\d+)\\s+([a-f0-9]{64})`, "i").exec(completed.output);
      assert(receipt && Number(receipt[1]) === bytes && receipt[2].toLowerCase() === expectedSha256, "Receiver commit receipt did not match", { path: absolute, operation: "write-file" });
      assert(completed.result?.exit_code === 0, "Receiver did not exit cleanly", { path: absolute, operation: "write-file" });
      return { path: absolute, bytes, sha256: expectedSha256 };
    } catch (error) {
      try {
        await tools.write_stdin({ session_id: sessionId, chars: `A${WRITE_STDIN_RECORD_SEPARATOR}`, yield_time_ms: 1000, max_output_tokens: 1000 });
      } catch {}
      await run(`/bin/rm -f -- ${shellQuote(temporary)}`, 1000);
      throw error;
    }
  };

  return {
    async readText(path) {
      const absolute = validateReadable(path);
      const size = await run(`/usr/bin/wc -c < ${shellQuote(absolute)}`, 1000);
      assert(size.exit_code === 0, `Could not stat ${absolute}`, { path: absolute, operation: "read-file" });
      const expectedBytes = Number(String(size.output).trim());
      assert(Number.isInteger(expectedBytes) && expectedBytes >= 0 && expectedBytes <= maxReadBytes, `Invalid or oversized read for ${absolute}`, { path: absolute, operation: "read-file" });
      const result = await run(`/bin/cat -- ${shellQuote(absolute)}`, Math.max(4000, Math.ceil(expectedBytes / 2)));
      assert(result.exit_code === 0 && utf8Bytes(result.output).length === expectedBytes, `Read of ${absolute} failed or was truncated`, { path: absolute, operation: "read-file" });
      return result.output;
    },
    async writeBatch(entries) {
      assert(Array.isArray(entries) && entries.length > 0, "writeBatch entries are required", { operation: "write-file" });
      const paths = entries.map((entry) => validateWritable(entry.path));
      assert(new Set(paths).size === paths.length, "writeBatch paths must be unique", { operation: "write-file" });
      const metadata = [];
      for (const entry of entries) metadata.push(await streamText(entry.path, entry.text));
      return metadata;
    },
    async exists(path) {
      const absolute = validateReadable(path);
      const result = await run(`/bin/test -e ${shellQuote(absolute)}`, 1000);
      if (result.exit_code === 0) return true;
      if (result.exit_code === 1) return false;
      throw new DocsTrustedReadBridgeError(`Could not inspect ${absolute}`, { path: absolute, operation: "inspect-file" });
    },
  };
}

function validateFileIO(fileIO) {
  assert(isRecord(fileIO), "fileIO is required", { operation: "validate-file-io" });
  assert(typeof fileIO.readText === "function", "fileIO.readText is required", { operation: "validate-file-io" });
  assert(typeof fileIO.writeBatch === "function", "fileIO.writeBatch is required", { operation: "validate-file-io" });
  assert(typeof fileIO.exists === "function", "fileIO.exists is required", { operation: "validate-file-io" });
  return fileIO;
}

function compactControlAwareness(trustedRead, normalized) {
  const inventory = trustedRead.derivedControlInventory;
  return {
    hasProtectedControls: inventory.hasProtectedControls,
    authoritativeDropdownCount: inventory.authoritativeDropdownCount,
    opaqueControlCount: inventory.opaqueControlCount,
    affectedParagraphCount: normalized.controlSummary.affectedParagraphs,
    nativeElementCounts: inventory.nativeElementCounts,
    preservationRequired: trustedRead.editGuidance.requiresTargetedPreservation,
    recommendedBehavior: trustedRead.editGuidance.recommendedBehavior,
  };
}

export async function executeDocsTrustedReadToFiles({
  documentId = null,
  documentUrl = null,
  tabId = null,
  outputDir,
  workspaceRoot,
  skillRoot,
  tools,
  fileIO = null,
  capabilityOverrides = {},
  maxReadBytes = DEFAULT_MAX_READ_BYTES,
  maxWriteBytes = DEFAULT_MAX_WRITE_BYTES,
} = {}) {
  assert(Boolean(documentId) !== Boolean(documentUrl), "Provide exactly one of documentId or documentUrl", { operation: "validate-input" });
  const workspace = normalizeAbsolutePath(workspaceRoot, "workspaceRoot");
  const skill = normalizeAbsolutePath(skillRoot, "skillRoot");
  const output = assertWithinRoot(outputDir, workspace, "outputDir");
  const wrapperPath = assertWithinRoot(`${skill}/host/docs-trusted-read-wrapper.mjs`, skill, "wrapperPath");
  const normalizerPath = assertWithinRoot(`${skill}/runtime/docs-read-normalizer.mjs`, skill, "normalizerPath");
  const io = fileIO ? validateFileIO(fileIO) : createToolBackedFileIO({
    tools,
    workspaceRoot: workspace,
    skillRoot: skill,
    receiverPath: `${skill}/host/docs-stdin-receiver.pl`,
    maxReadBytes,
    maxWriteBytes,
  });
  assert(!(await io.exists(output)), "Trusted-read output directory already exists", { path: output, operation: "validate-output" });

  let trustedRead = null;
  let normalizedResult = null;
  let caught = null;
  try {
    const [wrapper, normalizer] = await Promise.all([
      loadTrustedModule(io, wrapperPath, ["readGoogleDocWithControlInventory"], "trusted read wrapper"),
      loadTrustedModule(io, normalizerPath, ["normalizeGoogleDocumentRead", "renderNormalizedDocumentText"], "document read normalizer"),
    ]);
    trustedRead = await wrapper.readGoogleDocWithControlInventory({ documentId, documentUrl, tabId, tools, capabilityOverrides });
    normalizedResult = normalizer.normalizeGoogleDocumentRead({
      documentResult: trustedRead.documentResult,
      trustedReadContext: trustedRead,
      selectedTabId: tabId,
    });
  } catch (error) {
    caught = error;
  }

  const status = caught ? "failed" : "complete";
  const dataArtifacts = [];
  if (trustedRead && normalizedResult) {
    dataArtifacts.push(jsonArtifact(joinPath(output, "document-result.json"), trustedRead.documentResult));
    if (trustedRead.dropdownResult !== null && trustedRead.dropdownResult !== undefined) {
      dataArtifacts.push(jsonArtifact(joinPath(output, "dropdown-result.json"), trustedRead.dropdownResult));
    }
    dataArtifacts.push(jsonArtifact(joinPath(output, "control-inventory.json"), {
      version: trustedRead.version,
      kind: trustedRead.kind,
      target: trustedRead.target,
      dropdownMetadata: trustedRead.dropdownMetadata,
      derivedControlInventory: trustedRead.derivedControlInventory,
      editGuidance: trustedRead.editGuidance,
    }));
    dataArtifacts.push(jsonArtifact(joinPath(output, "document-outline.json"), normalizedResult.normalized));
    dataArtifacts.push(fileArtifact(joinPath(output, "document-text.md"), normalizedResult.markdown));
  }

  const controlAwareness = trustedRead && normalizedResult
    ? compactControlAwareness(trustedRead, normalizedResult.normalized)
    : null;
  const receipt = {
    bridgeVersion: BRIDGE_VERSION,
    kind: "google-docs-trusted-read-receipt",
    status,
    target: trustedRead?.target ?? { documentId, documentUrl, selectedTabId: tabId },
    dropdownMetadata: trustedRead?.dropdownMetadata ?? null,
    controlAwareness,
    warnings: trustedRead?.editGuidance?.warnings ?? [],
    error: caught ? compactError(caught) : null,
  };
  const receiptArtifact = jsonArtifact(joinPath(output, "receipt.json"), receipt);
  const committedArtifacts = [...dataArtifacts, receiptArtifact];
  await io.writeBatch(committedArtifacts.map(({ path, text }) => ({ path, text })));

  const files = Object.fromEntries(committedArtifacts.map((artifact) => {
    const key = artifact.path.slice(output.length + 1).replace(/\.[^.]+$/, "").replace(/-/g, "_");
    return [key, artifact.metadata];
  }));
  const manifest = {
    bridgeVersion: BRIDGE_VERSION,
    kind: "google-docs-trusted-read",
    status,
    outputDir: output,
    target: receipt.target,
    dropdownMetadata: receipt.dropdownMetadata,
    controlAwareness,
    warnings: receipt.warnings,
    files,
    error: receipt.error,
  };
  const manifestArtifact = jsonArtifact(joinPath(output, "manifest.json"), manifest);
  await io.writeBatch([{ path: manifestArtifact.path, text: manifestArtifact.text }]);
  return { ...manifest, manifestFile: manifestArtifact.metadata };
}

export { BRIDGE_VERSION };

SHA-256: 8ee2b99c05d1adcdd4a4ab71c4f904dbf4bead9cd1c4daab2dd2644b37c3fbd4