525 lines
19 KiB
JavaScript
525 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
import http from "node:http";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { isAbsolute, join, posix } from "node:path";
|
|
|
|
import {
|
|
OFFICE_FIXTURES,
|
|
XLSX_FIXTURE,
|
|
} from "../fixtures/ooxml-office-fixtures.mjs";
|
|
|
|
const OFFICE_TOOL_CALL_ID = "call_write_office_artifacts";
|
|
const OFFICE_TOOL_NAME = "bash";
|
|
const MATERIALIZED_PREFIX = ".opencode/openwork/inbox/chat-attachments/";
|
|
|
|
function parseArgs(argv) {
|
|
const args = new Map();
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const item = argv[index];
|
|
if (item.startsWith("--")) args.set(item.slice(2), argv[index + 1] ?? "");
|
|
}
|
|
return args;
|
|
}
|
|
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const mode = args.get("mode") === "xlsx" ? "xlsx" : "office";
|
|
const EXPECTED_FIXTURES = mode === "xlsx" ? { xlsx: XLSX_FIXTURE } : OFFICE_FIXTURES;
|
|
const ARTIFACT_PATHS = mode === "xlsx"
|
|
? { xlsx: "artifacts/RevenueWorkbook.xlsx" }
|
|
: { docx: "artifacts/QuarterlyBrief.docx", pptx: "artifacts/LaunchRoadmap.pptx" };
|
|
|
|
function sha256(buffer) {
|
|
return createHash("sha256").update(buffer).digest("hex");
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function stringField(record, names) {
|
|
for (const name of names) {
|
|
const value = record[name];
|
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function parseDataUrl(value) {
|
|
const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,([A-Za-z0-9+/=\s]+)$/);
|
|
if (!match) return null;
|
|
const mime = (match[1] ?? "application/octet-stream").trim().toLowerCase();
|
|
const data = Buffer.from(match[2].replace(/\s+/g, ""), "base64");
|
|
return { mime, data };
|
|
}
|
|
|
|
function looksBase64(value) {
|
|
const clean = value.replace(/\s+/g, "");
|
|
return clean.length > 32 && clean.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(clean);
|
|
}
|
|
|
|
function contextFromRecord(record, context) {
|
|
const filename = stringField(record, ["filename", "fileName", "name"]);
|
|
const mime = stringField(record, ["mime", "mimeType", "mediaType", "mime_type", "contentType"]);
|
|
return {
|
|
filename: filename || context.filename || "",
|
|
mime: mime.includes("/") ? mime.toLowerCase() : context.mime || "",
|
|
};
|
|
}
|
|
|
|
function addPayload(out, payload, context) {
|
|
if (!payload.data.byteLength) return;
|
|
out.push({
|
|
filename: context.filename,
|
|
mime: payload.mime || context.mime,
|
|
data: payload.data,
|
|
sha256: sha256(payload.data),
|
|
});
|
|
}
|
|
|
|
function extractPayloads(value, context = { filename: "", mime: "" }) {
|
|
const out = [];
|
|
if (typeof value === "string") {
|
|
const payload = parseDataUrl(value);
|
|
if (payload) addPayload(out, payload, context);
|
|
return out;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) out.push(...extractPayloads(item, context));
|
|
return out;
|
|
}
|
|
|
|
if (!isRecord(value)) return out;
|
|
|
|
const nextContext = contextFromRecord(value, context);
|
|
for (const key of ["file_data", "fileData", "data", "bytes", "contentBase64"]) {
|
|
const candidate = value[key];
|
|
if (typeof candidate !== "string") continue;
|
|
const dataUrl = parseDataUrl(candidate);
|
|
if (dataUrl) {
|
|
addPayload(out, dataUrl, nextContext);
|
|
} else if ((nextContext.filename || nextContext.mime) && looksBase64(candidate)) {
|
|
addPayload(out, { mime: nextContext.mime, data: Buffer.from(candidate.replace(/\s+/g, ""), "base64") }, nextContext);
|
|
}
|
|
}
|
|
|
|
const url = value.url;
|
|
if (typeof url === "string") {
|
|
const dataUrl = parseDataUrl(url);
|
|
if (dataUrl) addPayload(out, dataUrl, nextContext);
|
|
}
|
|
|
|
for (const item of Object.values(value)) {
|
|
out.push(...extractPayloads(item, nextContext));
|
|
}
|
|
|
|
const seen = new Set();
|
|
return out.filter((item) => {
|
|
const key = `${item.filename}|${item.mime}|${item.sha256}`;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function collectPromptText(value) {
|
|
if (typeof value === "string") return [value];
|
|
if (Array.isArray(value)) return value.flatMap(collectPromptText);
|
|
if (!isRecord(value)) return [];
|
|
if (typeof value.role === "string" && value.role !== "user") return [];
|
|
const type = typeof value.type === "string" ? value.type : "";
|
|
if (type === "text" && typeof value.text === "string") return [value.text];
|
|
if (value.role === "user" && typeof value.content === "string") return [value.content];
|
|
return Object.values(value).flatMap(collectPromptText);
|
|
}
|
|
|
|
function fieldValue(text, name) {
|
|
const prefix = `${name}: `;
|
|
const line = text.split("\n").find((item) => item.startsWith(prefix));
|
|
return line ? line.slice(prefix.length).trim() : "";
|
|
}
|
|
|
|
function safeWorkerRelativePath(value) {
|
|
const clean = String(value || "").trim().replaceAll("\\", "/");
|
|
if (!clean || clean === "unavailable" || clean.includes("\0") || clean.startsWith("/")) return "";
|
|
const normalized = posix.normalize(clean);
|
|
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || isAbsolute(normalized)) return "";
|
|
return normalized.startsWith(MATERIALIZED_PREFIX) ? normalized : "";
|
|
}
|
|
|
|
function readMaterializedHash(relativePath) {
|
|
if (!relativePath) return { exists: false, sha256: "" };
|
|
const absolutePath = join(workspaceRoot, relativePath);
|
|
if (!existsSync(absolutePath)) return { exists: false, sha256: "" };
|
|
const data = readFileSync(absolutePath);
|
|
return { exists: true, sha256: sha256(data), size: data.byteLength };
|
|
}
|
|
|
|
function countOfficeFileMarkers(value, context = { filename: "", mime: "" }) {
|
|
if (Array.isArray(value)) return value.reduce((sum, item) => sum + countOfficeFileMarkers(item, context), 0);
|
|
if (!isRecord(value)) return 0;
|
|
|
|
const nextContext = contextFromRecord(value, context);
|
|
const type = stringField(value, ["type"]).toLowerCase();
|
|
const officeLike = Object.values(EXPECTED_FIXTURES).some((expected) => nextContext.filename === expected.filename || nextContext.mime === expected.mime);
|
|
const fileLike = type.includes("file") || "file_data" in value || "fileData" in value || "contentBase64" in value;
|
|
let count = officeLike && fileLike ? 1 : 0;
|
|
for (const item of Object.values(value)) count += countOfficeFileMarkers(item, nextContext);
|
|
return count;
|
|
}
|
|
|
|
function parseNormalizedAttachment(texts, expected) {
|
|
const block = texts.find((text) => {
|
|
return text.includes("OpenWork normalized an Office attachment")
|
|
&& text.includes(`filename: ${expected.filename}`)
|
|
&& text.includes(`canonical_mime: ${expected.mime}`);
|
|
});
|
|
if (!block) return null;
|
|
|
|
const filename = fieldValue(block, "filename");
|
|
const mime = fieldValue(block, "canonical_mime");
|
|
const digest = fieldValue(block, "sha256");
|
|
const workerRelativePath = safeWorkerRelativePath(fieldValue(block, "worker_relative_path"));
|
|
const materialized = readMaterializedHash(workerRelativePath);
|
|
const sentinels = expected.sentinels.map((sentinel) => ({ sentinel, found: block.includes(sentinel) }));
|
|
return {
|
|
received: true,
|
|
filename,
|
|
mime,
|
|
sha256: digest,
|
|
workerRelativePath,
|
|
materializedSize: materialized.size ?? 0,
|
|
materializedSha256: materialized.sha256,
|
|
hashMatches: digest === expected.sha256,
|
|
materializedHashMatches: materialized.sha256 === expected.sha256,
|
|
mimeMatches: mime === expected.mime,
|
|
filenameMatches: filename === expected.filename,
|
|
pathSafe: Boolean(workerRelativePath),
|
|
materializedExists: materialized.exists,
|
|
sentinels,
|
|
};
|
|
}
|
|
|
|
function verifyOfficePayloads(body) {
|
|
const payloads = extractPayloads(body);
|
|
const promptTexts = collectPromptText(body);
|
|
const rawBody = JSON.stringify(body);
|
|
const officeFileMarkers = countOfficeFileMarkers(body);
|
|
const rawOfficeBinaryLeak = Object.values(EXPECTED_FIXTURES).some((expected) => {
|
|
return rawBody.includes(expected.dataBase64)
|
|
|| payloads.some((item) => item.sha256 === expected.sha256 || item.filename === expected.filename || item.mime === expected.mime);
|
|
});
|
|
const attachments = {};
|
|
|
|
for (const [kind, expected] of Object.entries(EXPECTED_FIXTURES)) {
|
|
attachments[kind] = parseNormalizedAttachment(promptTexts, expected) ?? { received: false, expected };
|
|
}
|
|
|
|
const normalizedTextOnly = payloads.length === 0 && officeFileMarkers === 0 && !rawOfficeBinaryLeak;
|
|
const values = Object.values(attachments);
|
|
return {
|
|
payloadCount: payloads.length,
|
|
officeFileMarkers,
|
|
rawOfficeBinaryLeak,
|
|
normalizedTextOnly,
|
|
promptTextCount: promptTexts.length,
|
|
attachments,
|
|
ok: normalizedTextOnly && values.every((item) => {
|
|
return item.received
|
|
&& item.hashMatches
|
|
&& item.materializedHashMatches
|
|
&& item.mimeMatches
|
|
&& item.filenameMatches
|
|
&& item.pathSafe
|
|
&& item.materializedExists
|
|
&& item.sentinels.every((sentinel) => sentinel.found);
|
|
}),
|
|
};
|
|
}
|
|
|
|
function collectStringValues(value) {
|
|
if (typeof value === "string") return [value];
|
|
if (Array.isArray(value)) return value.flatMap(collectStringValues);
|
|
if (!isRecord(value)) return [];
|
|
return Object.values(value).flatMap(collectStringValues);
|
|
}
|
|
|
|
function hasIssuedToolCallId(record) {
|
|
return [record.tool_call_id, record.toolCallId, record.tool_use_id, record.toolUseId, record.call_id, record.callId]
|
|
.some((value) => value === OFFICE_TOOL_CALL_ID);
|
|
}
|
|
|
|
function isToolResultRecord(record) {
|
|
const role = typeof record.role === "string" ? record.role : "";
|
|
const type = typeof record.type === "string" ? record.type : "";
|
|
return role === "tool" || type === "tool-result" || type === "tool_result" || type === "toolResult";
|
|
}
|
|
|
|
function hasExpectedToolOutput(record) {
|
|
const text = collectStringValues(record).join("\n");
|
|
return Object.values(ARTIFACT_PATHS).every((path) => text.includes(path))
|
|
&& Object.values(EXPECTED_FIXTURES).every((expected) => text.includes(expected.sha256));
|
|
}
|
|
|
|
function hasExactIssuedToolResult(value) {
|
|
if (Array.isArray(value)) return value.some(hasExactIssuedToolResult);
|
|
if (!isRecord(value)) return false;
|
|
if (isToolResultRecord(value) && hasIssuedToolCallId(value) && hasExpectedToolOutput(value)) return true;
|
|
return Object.values(value).some(hasExactIssuedToolResult);
|
|
}
|
|
|
|
function writeSse(res, chunks) {
|
|
res.writeHead(200, {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
});
|
|
for (const chunk of chunks) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
res.write("data: [DONE]\n\n");
|
|
res.end();
|
|
}
|
|
|
|
function textStream(text, id = "chatcmpl-office-text") {
|
|
return [
|
|
{ id, object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }] },
|
|
{ id, object: "chat.completion.chunk", choices: [{ index: 0, delta: { content: text }, finish_reason: null }] },
|
|
{ id, object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] },
|
|
];
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
}
|
|
|
|
function artifactWriteCommand(attachments) {
|
|
const entries = Object.entries(ARTIFACT_PATHS).map(([kind, path]) => {
|
|
const source = attachments[kind]?.workerRelativePath;
|
|
if (!source) throw new Error(`Missing materialized Office path for ${kind}: ${JSON.stringify(attachments)}`);
|
|
return { source, path };
|
|
});
|
|
const paths = entries.map((entry) => shellQuote(entry.path)).join(" ");
|
|
return `set -euo pipefail
|
|
mkdir -p artifacts
|
|
${entries.map((entry) => `test -s ${shellQuote(entry.source)}\ncp ${shellQuote(entry.source)} ${shellQuote(entry.path)}`).join("\n")}
|
|
(sha256sum ${paths} 2>/dev/null || shasum -a 256 ${paths})`;
|
|
}
|
|
|
|
function toolCallStream(attachments) {
|
|
return [
|
|
{ id: "chatcmpl-office-tool", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }] },
|
|
{
|
|
id: "chatcmpl-office-tool",
|
|
object: "chat.completion.chunk",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: 0,
|
|
id: OFFICE_TOOL_CALL_ID,
|
|
type: "function",
|
|
function: {
|
|
name: OFFICE_TOOL_NAME,
|
|
arguments: JSON.stringify({
|
|
description: "Write exact materialized Office attachments as workspace artifacts",
|
|
command: artifactWriteCommand(attachments),
|
|
timeout: 10000,
|
|
}),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
},
|
|
{ id: "chatcmpl-office-tool", object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] },
|
|
];
|
|
}
|
|
|
|
function finalText() {
|
|
return [
|
|
...Object.values(EXPECTED_FIXTURES).map((expected) => `Verified ${expected.filename}: ${expected.sentinels.join(" | ")}`),
|
|
`Created ${Object.values(ARTIFACT_PATHS).join(" and ")} from the exact safely materialized Office bytes.`,
|
|
].join("\n");
|
|
}
|
|
|
|
const host = args.get("host") || "127.0.0.1";
|
|
const port = Number(args.get("port") || 18081);
|
|
const workspaceRoot = args.get("workspace") || process.cwd();
|
|
const sockets = new Set();
|
|
let server;
|
|
|
|
const proof = {
|
|
ok: true,
|
|
requests: 0,
|
|
providerReceipt: false,
|
|
normalizedTextOnly: false,
|
|
exactHashes: false,
|
|
exactMimes: false,
|
|
materializedPaths: false,
|
|
sentinelsExtracted: false,
|
|
rawOfficeBinaryLeak: false,
|
|
officeFileMarkers: 0,
|
|
payloadCount: 0,
|
|
toolCallIssued: false,
|
|
toolCallCompleted: false,
|
|
finalResponse: false,
|
|
replayResponse: false,
|
|
replayOfficeHistoryOk: false,
|
|
auxiliaryRequests: 0,
|
|
auxiliaryBeforeFinal: 0,
|
|
auxiliaryAfterFinal: 0,
|
|
lastAuxiliaryRequest: null,
|
|
attachments: {},
|
|
replay: null,
|
|
errors: [],
|
|
};
|
|
|
|
async function readBody(req) {
|
|
return await new Promise((resolve) => {
|
|
let raw = "";
|
|
req.setEncoding("utf8");
|
|
req.on("data", (chunk) => {
|
|
raw += chunk;
|
|
});
|
|
req.on("end", () => resolve(raw));
|
|
});
|
|
}
|
|
|
|
function updateProofFromVerification(verification, replay = false) {
|
|
if (replay) {
|
|
proof.replay = verification;
|
|
proof.replayOfficeHistoryOk = verification.ok;
|
|
return;
|
|
}
|
|
proof.providerReceipt = verification.ok;
|
|
proof.normalizedTextOnly = verification.normalizedTextOnly;
|
|
proof.exactHashes = Object.values(verification.attachments).every((item) => item.hashMatches === true && item.materializedHashMatches === true);
|
|
proof.exactMimes = Object.values(verification.attachments).every((item) => item.mimeMatches === true);
|
|
proof.materializedPaths = Object.values(verification.attachments).every((item) => item.pathSafe === true && item.materializedExists === true);
|
|
proof.sentinelsExtracted = Object.values(verification.attachments).every((item) => item.sentinels?.every((sentinel) => sentinel.found));
|
|
proof.rawOfficeBinaryLeak = verification.rawOfficeBinaryLeak;
|
|
proof.officeFileMarkers = verification.officeFileMarkers;
|
|
proof.payloadCount = verification.payloadCount;
|
|
proof.attachments = verification.attachments;
|
|
}
|
|
|
|
function recordAuxiliaryRequest(phase, reason) {
|
|
proof.auxiliaryRequests += 1;
|
|
if (phase === "before_final") proof.auxiliaryBeforeFinal += 1;
|
|
if (phase === "after_final") proof.auxiliaryAfterFinal += 1;
|
|
proof.lastAuxiliaryRequest = {
|
|
request: proof.requests,
|
|
phase,
|
|
reason,
|
|
};
|
|
}
|
|
|
|
function writeAuxiliaryResponse(res, phase, reason) {
|
|
recordAuxiliaryRequest(phase, reason);
|
|
writeSse(res, textStream("Office attachments", "chatcmpl-office-auxiliary"));
|
|
}
|
|
|
|
function sendJson(res, payload, status = 200) {
|
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify(payload, null, 2));
|
|
}
|
|
|
|
async function handleChatCompletion(req, res) {
|
|
proof.requests += 1;
|
|
const raw = await readBody(req);
|
|
let body = {};
|
|
try {
|
|
body = raw ? JSON.parse(raw) : {};
|
|
} catch (error) {
|
|
proof.errors.push(`invalid JSON request: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
|
|
try {
|
|
if (!proof.toolCallIssued) {
|
|
const verification = verifyOfficePayloads(body);
|
|
updateProofFromVerification(verification, false);
|
|
if (!verification.ok) {
|
|
proof.ok = false;
|
|
proof.errors.push(`initial normalized Office verification failed: ${JSON.stringify(verification)}`);
|
|
writeSse(res, textStream("Office attachment normalization verification failed before tool execution."));
|
|
return;
|
|
}
|
|
proof.toolCallIssued = true;
|
|
writeSse(res, toolCallStream(verification.attachments));
|
|
return;
|
|
}
|
|
|
|
if (!proof.finalResponse) {
|
|
if (!hasExactIssuedToolResult(body)) {
|
|
writeAuxiliaryResponse(res, "before_final", "missing exact issued tool result");
|
|
return;
|
|
}
|
|
proof.toolCallCompleted = true;
|
|
proof.finalResponse = true;
|
|
writeSse(res, textStream(finalText(), "chatcmpl-office-final"));
|
|
return;
|
|
}
|
|
|
|
if (proof.replayResponse) {
|
|
writeAuxiliaryResponse(res, "after_final", "replay response already completed");
|
|
return;
|
|
}
|
|
|
|
const replayVerification = verifyOfficePayloads(body);
|
|
if (!replayVerification.ok) {
|
|
writeAuxiliaryResponse(res, "after_final", "missing verified normalized Office history");
|
|
return;
|
|
}
|
|
updateProofFromVerification(replayVerification, true);
|
|
proof.replayResponse = true;
|
|
writeSse(res, textStream("Replay succeeded after reopening the session; Office attachment history remained readable and the follow-up was answered.", "chatcmpl-office-replay"));
|
|
} catch (error) {
|
|
proof.ok = false;
|
|
proof.errors.push("Office attachment mock request failed.");
|
|
console.error("[office-attachments-mock-provider] chat completion failed", error);
|
|
sendJson(res, { error: "Office attachment mock request failed." }, 500);
|
|
}
|
|
}
|
|
|
|
server = http.createServer(async (req, res) => {
|
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? `${host}:${port}`}`);
|
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
sendJson(res, { ok: true, port });
|
|
return;
|
|
}
|
|
if (req.method === "GET" && url.pathname === "/proof") {
|
|
sendJson(res, proof);
|
|
return;
|
|
}
|
|
if (req.method === "POST" && url.pathname === "/shutdown") {
|
|
sendJson(res, { ok: true });
|
|
setTimeout(() => {
|
|
for (const socket of sockets) socket.destroy();
|
|
server.close(() => process.exit(0));
|
|
}, 50);
|
|
return;
|
|
}
|
|
if (req.method === "GET" && (url.pathname === "/v1/models" || url.pathname === "/models")) {
|
|
sendJson(res, { object: "list", data: [{ id: "office-attachment-mock", object: "model" }] });
|
|
return;
|
|
}
|
|
if (req.method === "POST" && (url.pathname === "/v1/chat/completions" || url.pathname === "/chat/completions")) {
|
|
await handleChatCompletion(req, res);
|
|
return;
|
|
}
|
|
sendJson(res, { error: "not found" }, 404);
|
|
});
|
|
|
|
server.on("connection", (socket) => {
|
|
sockets.add(socket);
|
|
socket.on("close", () => sockets.delete(socket));
|
|
});
|
|
|
|
server.listen(port, host, () => {
|
|
console.log(JSON.stringify({ ok: true, host, port }));
|
|
});
|