Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
import { getQuickJS } from "quickjs-emscripten";
|
||
import { methods } from "./api.mjs";
|
||
import { MAX_BRIDGE_BYTES as MAX_BRIDGE, EXECUTION_LIMITS } from "./limits.mjs";
|
||
import { serializeJson } from "./json-capacity.mjs";
|
||
import { evidenceIds, serializationFailure } from "./fact-provenance.mjs";
|
||
import { projectFactFailure } from "./fact-error.mjs";
|
||
|
||
const MAX_OUTPUT = EXECUTION_LIMITS.output_utf8_bytes;
|
||
const failure = (code, message) => ({ ok: false, error: { code, message } });
|
||
const projectGuestError = (code, error) => {
|
||
const message =
|
||
typeof error?.message === "string" ? error.message : String(error);
|
||
// Check raw size first: JSON escaping expands each code unit at most sixfold,
|
||
// so the second check has a 6×32 KiB string bound plus a fixed envelope.
|
||
if (
|
||
Buffer.byteLength(message) > MAX_OUTPUT ||
|
||
Buffer.byteLength(JSON.stringify({ code, message })) > MAX_OUTPUT
|
||
)
|
||
return {
|
||
code: "output_budget",
|
||
message: `JavaScript error exceeds the ${MAX_OUTPUT / 1024} KiB output budget; its message is omitted. Native Evidence remains retained.`,
|
||
details: {
|
||
original_error_code: code,
|
||
original_message_code_units: message.length,
|
||
original_message_omitted: true,
|
||
},
|
||
};
|
||
return { code, message };
|
||
};
|
||
export const executeJavaScript = async (
|
||
session,
|
||
code,
|
||
{
|
||
signal = new AbortController().signal,
|
||
timeoutMs = EXECUTION_LIMITS.timeout_ms.default,
|
||
cpuBudgetMs = EXECUTION_LIMITS.execution_cpu_ms,
|
||
} = {},
|
||
) => {
|
||
if (Buffer.byteLength(code) > EXECUTION_LIMITS.code_utf8_bytes)
|
||
return failure(
|
||
"code_budget",
|
||
`JavaScript source exceeds ${EXECUTION_LIMITS.code_utf8_bytes / 1024} KiB`,
|
||
);
|
||
const started = performance.now();
|
||
const context = session.context;
|
||
const lease = context?.lease;
|
||
const stop = new AbortController();
|
||
const combined = AbortSignal.any([signal, stop.signal]);
|
||
const deadline = Date.now() + timeoutMs;
|
||
const timer = setTimeout(() => stop.abort(), timeoutMs);
|
||
const QuickJS = await getQuickJS();
|
||
const runtime = QuickJS.newRuntime();
|
||
runtime.setMemoryLimit(EXECUTION_LIMITS.execution_heap_mib * 1024 * 1024);
|
||
runtime.setMaxStackSize(EXECUTION_LIMITS.execution_stack_bytes);
|
||
const budget = { used: 0, deadline: 0, exhausted: false };
|
||
const runGuest = (work) => {
|
||
budget.deadline = Date.now() + Math.max(1, cpuBudgetMs - budget.used);
|
||
const before = performance.now();
|
||
try {
|
||
return work();
|
||
} finally {
|
||
budget.used += performance.now() - before;
|
||
}
|
||
};
|
||
runtime.setInterruptHandler(() => {
|
||
budget.exhausted ||= Date.now() >= budget.deadline;
|
||
return budget.exhausted || combined.aborted || Date.now() >= deadline;
|
||
});
|
||
const vm = runtime.newContext();
|
||
const pending = new Set();
|
||
const calls = [];
|
||
const state = { disposed: false, evidence: new Set() };
|
||
const pump = () => {
|
||
if (!state.disposed) {
|
||
const result = runGuest(() => runtime.executePendingJobs());
|
||
if (result.error) {
|
||
result.error.dispose();
|
||
stop.abort();
|
||
}
|
||
}
|
||
};
|
||
installBridge(vm, session, { combined, lease, calls, pending, pump, state });
|
||
let outcome;
|
||
try {
|
||
const text = await evaluateGuest(vm, code, { runGuest, pump, combined });
|
||
if (Buffer.byteLength(text) > MAX_OUTPUT)
|
||
outcome = {
|
||
ok: false,
|
||
error: {
|
||
code: "output_budget",
|
||
message: `Return a smaller projection (${MAX_OUTPUT / 1024} KiB UTF-8 output budget); native Evidence remains retained`,
|
||
},
|
||
};
|
||
else
|
||
outcome = {
|
||
ok: true,
|
||
value: text === "undefined" ? null : JSON.parse(text),
|
||
};
|
||
} catch (error) {
|
||
outcome = {
|
||
ok: false,
|
||
error: projectGuestError(
|
||
budget.exhausted
|
||
? "cpu_budget"
|
||
: combined.aborted
|
||
? "cancelled"
|
||
: "javascript_error",
|
||
budget.exhausted
|
||
? "Guest synchronous execution budget exceeded"
|
||
: error,
|
||
),
|
||
};
|
||
} finally {
|
||
clearTimeout(timer);
|
||
stop.abort();
|
||
await Promise.allSettled(pending);
|
||
state.disposed = true;
|
||
vm.dispose();
|
||
runtime.dispose();
|
||
}
|
||
return {
|
||
...outcome,
|
||
context,
|
||
evidence_ids: [...state.evidence],
|
||
calls,
|
||
elapsed_ms: performance.now() - started,
|
||
};
|
||
};
|
||
|
||
const prepareBridgeResponse = (response) => {
|
||
const encoded = serializeJson(response, MAX_BRIDGE);
|
||
if (encoded.ok) return { response, text: encoded.text };
|
||
const delivered = serializationFailure(
|
||
response,
|
||
encoded.error,
|
||
"quickjs_bridge",
|
||
"bridge_budget",
|
||
),
|
||
bounded = serializeJson(delivered, MAX_BRIDGE);
|
||
if (!bounded.ok)
|
||
throw new Error("Bridge capacity diagnostic could not be encoded");
|
||
return { response: delivered, text: bounded.text };
|
||
};
|
||
|
||
const installBridge = (
|
||
vm,
|
||
session,
|
||
{ combined, lease, calls, pending, pump, state },
|
||
) => {
|
||
const bridge = vm.newFunction("analysis", (methodHandle, inputHandle) => {
|
||
const name = vm.getString(methodHandle);
|
||
const inputText = vm.getString(inputHandle);
|
||
if (
|
||
calls.length >= EXECUTION_LIMITS.max_host_calls ||
|
||
Buffer.byteLength(inputText) > MAX_BRIDGE
|
||
)
|
||
throw new Error("Host call or argument budget exceeded");
|
||
const promise = vm.newPromise();
|
||
const index = calls.length;
|
||
const started = performance.now();
|
||
calls.push({
|
||
method: name,
|
||
input: JSON.parse(inputText),
|
||
status: "pending",
|
||
});
|
||
const work = Promise.resolve()
|
||
.then(async () => {
|
||
const response = await session.call(
|
||
name,
|
||
JSON.parse(inputText),
|
||
combined,
|
||
lease,
|
||
);
|
||
for (const id of evidenceIds(response)) state.evidence.add(id);
|
||
const { response: delivered, text } = prepareBridgeResponse(response);
|
||
calls[index].status = delivered.ok ? "ok" : "error";
|
||
if (!delivered.ok) calls[index].error = delivered.error;
|
||
calls[index].elapsed_ms = performance.now() - started;
|
||
if (!state.disposed) {
|
||
const value = vm.newString(text);
|
||
promise.resolve(value);
|
||
value.dispose();
|
||
pump();
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
const { response, text } = prepareBridgeResponse({
|
||
ok: false,
|
||
context: session.context,
|
||
error: projectFactFailure(error, "bridge_failed"),
|
||
});
|
||
for (const id of evidenceIds(response)) state.evidence.add(id);
|
||
calls[index].status = "error";
|
||
calls[index].error = response.error;
|
||
calls[index].elapsed_ms = performance.now() - started;
|
||
if (!state.disposed) {
|
||
const value = vm.newString(text);
|
||
promise.resolve(value);
|
||
value.dispose();
|
||
pump();
|
||
}
|
||
})
|
||
.finally(() => pending.delete(work));
|
||
pending.add(work);
|
||
return promise.handle;
|
||
});
|
||
vm.setProp(vm.global, "__analysis", bridge);
|
||
bridge.dispose();
|
||
};
|
||
|
||
const evaluateGuest = async (vm, code, { runGuest, pump, combined }) => {
|
||
const bootstrap = `globalThis.rea=Object.freeze(Object.fromEntries(${JSON.stringify(Object.keys(methods))}.map(name=>[name,async(input={})=>JSON.parse(await __analysis(name,JSON.stringify(input)))])));`;
|
||
vm.unwrapResult(runGuest(() => vm.evalCode(bootstrap))).dispose();
|
||
const promiseHandle = vm.unwrapResult(
|
||
runGuest(() =>
|
||
vm.evalCode(
|
||
`(async()=>JSON.stringify(await(async()=>{\n${code}\n})()))()`,
|
||
),
|
||
),
|
||
);
|
||
try {
|
||
const resolved = vm.resolvePromise(promiseHandle);
|
||
pump();
|
||
// A never-settling guest Promise must not hang the MCP request forever.
|
||
const aborted = new Promise((_, reject) => {
|
||
if (combined.aborted) {
|
||
reject(new Error("Execution cancelled"));
|
||
return;
|
||
}
|
||
combined.addEventListener(
|
||
"abort",
|
||
() => reject(new Error("Execution cancelled or timed out")),
|
||
{ once: true },
|
||
);
|
||
});
|
||
const result = await Promise.race([resolved, aborted]);
|
||
const value = vm.unwrapResult(result);
|
||
const text = vm.getString(value);
|
||
value.dispose();
|
||
return text;
|
||
} finally {
|
||
promiseHandle.dispose();
|
||
}
|
||
};
|