Skip to content

Commit 6ce063a

Browse files
authored
refactor(v8-bridge): split the bridge bundle into a documented multi-file TypeScript package (#164)
1 parent 6dc2e3e commit 6ce063a

26 files changed

Lines changed: 55872 additions & 26789 deletions

crates/execution/assets/v8-bridge.source.js

Lines changed: 26535 additions & 26632 deletions
Large diffs are not rendered by default.

crates/v8-runtime/assets/generated/v8-bridge.js

Lines changed: 141 additions & 141 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/browser/scripts/check-bridge-contract.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ const browserFacadeContractGlobals = new Set([
118118
// Contracted native bridge names that are not browser-installed yet. Keeping
119119
// this list explicit makes browser contract coverage drift fail closed.
120120
const browserUnsupportedContractGlobals = new Set([
121+
"_benchNoop",
122+
"_benchNetTcpMetricsResetRaw",
123+
"_benchNetTcpMetricsSnapshotRaw",
121124
"_dynamicImport",
122125
"_kernelIsattyRaw",
123126
"_kernelTtySizeRaw",

packages/browser/src/generated/util-polyfill.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/browser/src/wasi-polyfill.ts

Lines changed: 189 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
179179
const __agentOSKernelStdioSyncRpcEnabled = () =>
180180
process?.env?.AGENTOS_WASI_STDIO_SYNC_RPC === "1";
181181
const __agentOSWasiDebugEnabled = () => process?.env?.AGENTOS_WASM_WASI_DEBUG === "1";
182+
const __agentOSWasiSyscallCountersEnabled = () =>
183+
process?.env?.AGENTOS_WASI_SYSCALL_COUNTERS === "1";
184+
const __agentOSWasiNow = () =>
185+
typeof performance?.now === "function" ? performance.now() : Date.now();
182186
const __agentOSWasiDebug = (message) => {
183187
if (!__agentOSWasiDebugEnabled() || typeof process?.stderr?.write !== "function") {
184188
return;
@@ -208,6 +212,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
208212
[1, { kind: "stdout", fdFlags: 0 }],
209213
[2, { kind: "stderr", fdFlags: 0 }],
210214
]);
215+
this.syscallCountersEnabled = __agentOSWasiSyscallCountersEnabled();
211216
for (const [guestPath, spec] of Object.entries(this.preopens)) {
212217
const normalized = this._normalizePreopenSpec(spec);
213218
if (!normalized) {
@@ -259,6 +264,70 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
259264
random_get: (...args) => this._randomGet(...args),
260265
sched_yield: (...args) => this._schedYield(...args),
261266
};
267+
this._installSyscallCounterWrappers();
268+
}
269+
270+
_recordWasiSyscallMetric(name, startedAt, details = {}) {
271+
if (!this.syscallCountersEnabled || typeof process?.stderr?.write !== "function") {
272+
return;
273+
}
274+
try {
275+
const elapsedMs = __agentOSWasiNow() - startedAt;
276+
process.stderr.write(
277+
\`__AGENTOS_WASI_SYSCALL_METRICS__:\${JSON.stringify({
278+
name,
279+
elapsedMs,
280+
...details,
281+
})}\\n\`,
282+
);
283+
} catch {
284+
// Ignore metrics failures.
285+
}
286+
}
287+
288+
_installSyscallCounterWrappers() {
289+
if (!this.syscallCountersEnabled) {
290+
return;
291+
}
292+
for (const name of [
293+
"path_open",
294+
"path_filestat_get",
295+
"fd_filestat_get",
296+
"fd_write",
297+
]) {
298+
const original = this.wasiImport[name];
299+
if (typeof original !== "function") {
300+
continue;
301+
}
302+
this.wasiImport[name] = (...args) => {
303+
const startedAt = __agentOSWasiNow();
304+
const result = original(...args);
305+
const details = {
306+
result,
307+
fd: Number(args[0]) >>> 0,
308+
iovsLen: name === "fd_write" ? Number(args[2]) >>> 0 : undefined,
309+
};
310+
if (name === "path_filestat_get") {
311+
try {
312+
const target = this._readString(args[2], args[3]);
313+
details.pathLen = target.length;
314+
details.pathKind = target.startsWith("/")
315+
? "absolute"
316+
: target.includes("/")
317+
? "relative-nested"
318+
: "relative-child";
319+
details.pathSample =
320+
target.length > 120 ? \`\${target.slice(0, 120)}...\` : target;
321+
} catch {
322+
// Leave path-shape fields absent if the guest pointer is invalid.
323+
}
324+
}
325+
this._recordWasiSyscallMetric(name, startedAt, {
326+
...details,
327+
});
328+
return result;
329+
};
330+
}
262331
}
263332
264333
start(instance) {
@@ -987,6 +1056,22 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
9871056
);
9881057
}
9891058
1059+
_rootRelativeTargetIsWithinAbsoluteArg(target) {
1060+
const rootGuestPath = __agentOSPath().posix.resolve("/", target);
1061+
return this.args
1062+
.slice(1)
1063+
.some((arg) => {
1064+
if (typeof arg !== "string" || !arg.startsWith("/")) {
1065+
return false;
1066+
}
1067+
const normalizedArg = __agentOSPath().posix.normalize(arg);
1068+
return (
1069+
rootGuestPath === normalizedArg ||
1070+
rootGuestPath.startsWith(\`\${normalizedArg}/\`)
1071+
);
1072+
});
1073+
}
1074+
9901075
_resolveRootRelativePath(target, preferCreateParent = false) {
9911076
const rootGuestPath = __agentOSPath().posix.resolve("/", target);
9921077
const rootMapping = this._resolveHostMappingForGuestPath(rootGuestPath);
@@ -1066,6 +1151,44 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
10661151
return resolved?.hostPath ?? null;
10671152
}
10681153
1154+
_resolveDescriptorDirectStatPath(fd, target) {
1155+
if (
1156+
typeof target !== "string" ||
1157+
target.length === 0 ||
1158+
target === "." ||
1159+
target === ".." ||
1160+
target.startsWith("/")
1161+
) {
1162+
return null;
1163+
}
1164+
const entry = this._descriptorEntry(fd);
1165+
if (
1166+
!entry ||
1167+
(entry.kind !== "directory" && entry.kind !== "preopen") ||
1168+
typeof entry.hostPath !== "string" ||
1169+
entry.hostPath.length === 0
1170+
) {
1171+
return null;
1172+
}
1173+
const baseGuestPath = this._descriptorGuestPath(entry);
1174+
if (typeof baseGuestPath !== "string") {
1175+
return null;
1176+
}
1177+
if (entry.kind === "preopen" && baseGuestPath === "/") {
1178+
if (!this._rootRelativeTargetIsWithinAbsoluteArg(target)) {
1179+
return null;
1180+
}
1181+
} else if (target.includes("/")) {
1182+
return null;
1183+
}
1184+
return {
1185+
error: __agentOSWasiErrnoSuccess,
1186+
guestPath: __agentOSPath().posix.resolve(baseGuestPath, target),
1187+
hostPath: __agentOSPath().join(entry.hostPath, target),
1188+
readOnly: entry.readOnly === true,
1189+
};
1190+
}
1191+
10691192
_writeFilestat(statPtr, stats, fallbackType) {
10701193
try {
10711194
const view = this._memoryView();
@@ -1752,6 +1875,9 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
17521875
}
17531876
17541877
_fdReaddir(fd, bufPtr, bufLen, cookie, bufUsedPtr) {
1878+
const startedAt = __agentOSWasiNow();
1879+
const requestedCookie = Number(cookie) >>> 0;
1880+
const requestedBufLen = Number(bufLen) >>> 0;
17551881
try {
17561882
const entry = this._descriptorEntry(fd);
17571883
const fsPath = this._descriptorDirectoryFsPath(entry);
@@ -1762,19 +1888,53 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
17621888
) {
17631889
return __agentOSWasiErrnoBadf;
17641890
}
1765-
const dirents = __agentOSFs()
1766-
.readdirSync(fsPath, { withFileTypes: true })
1767-
.sort((left, right) => left.name.localeCompare(right.name));
1891+
let dirents =
1892+
requestedCookie > 0 && Array.isArray(entry.readdirCache)
1893+
? entry.readdirCache
1894+
: null;
1895+
if (!dirents) {
1896+
dirents = __agentOSFs()
1897+
.readdirSync(fsPath, { withFileTypes: true })
1898+
.sort((left, right) => left.name.localeCompare(right.name));
1899+
entry.readdirCache = dirents;
1900+
}
17681901
const view = this._memoryView();
17691902
const memory = this._memoryBytes();
17701903
let offset = Number(bufPtr) >>> 0;
1771-
const limit = offset + (Number(bufLen) >>> 0);
1904+
const limit = offset + requestedBufLen;
17721905
let used = 0;
1773-
for (let index = Number(cookie) >>> 0; index < dirents.length; index += 1) {
1906+
let recordsReturned = 0;
1907+
let stoppedRecordTooLarge = false;
1908+
for (let index = requestedCookie; index < dirents.length; index += 1) {
17741909
const dirent = dirents[index];
17751910
const nameBytes = Buffer.from(dirent.name, "utf8");
17761911
const recordLen = 24 + nameBytes.length;
17771912
if (offset + recordLen > limit) {
1913+
const remaining = Math.max(0, limit - offset);
1914+
if (remaining > 0) {
1915+
const record = Buffer.alloc(recordLen);
1916+
const recordView = new DataView(
1917+
record.buffer,
1918+
record.byteOffset,
1919+
record.byteLength,
1920+
);
1921+
recordView.setBigUint64(0, BigInt(index + 1), true);
1922+
recordView.setBigUint64(8, BigInt(index + 1), true);
1923+
recordView.setUint32(16, nameBytes.length, true);
1924+
recordView.setUint8(
1925+
20,
1926+
dirent.isDirectory()
1927+
? __agentOSWasiFiletypeDirectory
1928+
: dirent.isSymbolicLink()
1929+
? __agentOSWasiFiletypeSymbolicLink
1930+
: __agentOSWasiFiletypeRegularFile,
1931+
);
1932+
record.set(nameBytes, 24);
1933+
memory.set(record.subarray(0, remaining), offset);
1934+
offset += remaining;
1935+
used += remaining;
1936+
}
1937+
stoppedRecordTooLarge = true;
17781938
break;
17791939
}
17801940
view.setBigUint64(offset, BigInt(index + 1), true);
@@ -1791,9 +1951,27 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
17911951
memory.set(nameBytes, offset + 24);
17921952
offset += recordLen;
17931953
used += recordLen;
1794-
}
1795-
return this._writeUint32(bufUsedPtr, used);
1954+
recordsReturned += 1;
1955+
}
1956+
const result = this._writeUint32(bufUsedPtr, used);
1957+
this._recordWasiSyscallMetric("fd_readdir", startedAt, {
1958+
result,
1959+
fd: Number(fd) >>> 0,
1960+
cookie: requestedCookie,
1961+
bufLen: requestedBufLen,
1962+
used,
1963+
recordsReturned,
1964+
totalDirentsRead: dirents.length,
1965+
stoppedRecordTooLarge,
1966+
});
1967+
return result;
17961968
} catch (error) {
1969+
this._recordWasiSyscallMetric("fd_readdir", startedAt, {
1970+
result: "error",
1971+
fd: Number(fd) >>> 0,
1972+
cookie: requestedCookie,
1973+
bufLen: requestedBufLen,
1974+
});
17971975
return this._mapFsError(error);
17981976
}
17991977
}
@@ -1997,7 +2175,10 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
19972175
19982176
_pathFilestatGet(fd, flags, pathPtr, pathLen, statPtr) {
19992177
try {
2000-
const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen);
2178+
const target = this._readString(pathPtr, pathLen);
2179+
const resolved =
2180+
this._resolveDescriptorDirectStatPath(fd, target) ??
2181+
this._resolveDescriptorPath(fd, pathPtr, pathLen);
20012182
if (resolved.error !== __agentOSWasiErrnoSuccess) {
20022183
return resolved.error;
20032184
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { _exited } from "./process.js";
2+
import { exposeCustomGlobal } from "../global-exposure.js";
3+
import { bridgeDispatchSync } from "../transport.js";
4+
5+
// .agent/recovery/secure-exec/nodejs/src/bridge/active-handles.ts
6+
var HANDLE_DISPATCH = {
7+
register: "kernelHandleRegister",
8+
unregister: "kernelHandleUnregister",
9+
list: "kernelHandleList"
10+
};
11+
var _activeHandles = /* @__PURE__ */ new Map();
12+
var _waitResolvers = [];
13+
function _registerHandle2(id, description) {
14+
try {
15+
bridgeDispatchSync(HANDLE_DISPATCH.register, id, description);
16+
} catch (error) {
17+
if (error instanceof Error && error.message.includes("EAGAIN")) {
18+
throw new Error(
19+
"ERR_RESOURCE_BUDGET_EXCEEDED: maximum active handles exceeded"
20+
);
21+
}
22+
throw error;
23+
}
24+
_activeHandles.set(id, description);
25+
}
26+
function _unregisterHandle2(id) {
27+
_activeHandles.delete(id);
28+
let remaining = _activeHandles.size;
29+
try {
30+
bridgeDispatchSync(HANDLE_DISPATCH.unregister, id);
31+
} catch {
32+
}
33+
if (remaining === 0 && _waitResolvers.length > 0) {
34+
const resolvers = _waitResolvers;
35+
_waitResolvers = [];
36+
resolvers.forEach((r) => r());
37+
}
38+
}
39+
function _waitForActiveHandles() {
40+
if (typeof _exited !== "undefined" && _exited) {
41+
return Promise.resolve();
42+
}
43+
const getPendingTimerCount = globalThis._getPendingTimerCount;
44+
const waitForTimerDrain = globalThis._waitForTimerDrain;
45+
const hasHandles = _getActiveHandles().length > 0;
46+
const hasTimers = typeof getPendingTimerCount === "function" && getPendingTimerCount() > 0;
47+
if (!hasHandles && !hasTimers) {
48+
return Promise.resolve();
49+
}
50+
const promises = [];
51+
if (hasHandles) {
52+
promises.push(
53+
new Promise((resolve) => {
54+
let settled = false;
55+
const complete = () => {
56+
if (settled) return;
57+
settled = true;
58+
resolve();
59+
};
60+
_waitResolvers.push(complete);
61+
if (_getActiveHandles().length === 0) {
62+
complete();
63+
}
64+
})
65+
);
66+
}
67+
if (hasTimers && typeof waitForTimerDrain === "function") {
68+
promises.push(waitForTimerDrain());
69+
}
70+
return Promise.all(promises).then(() => {
71+
});
72+
}
73+
function _getActiveHandles() {
74+
return Array.from(_activeHandles.values());
75+
}
76+
exposeCustomGlobal("_registerHandle", _registerHandle2);
77+
exposeCustomGlobal("_unregisterHandle", _unregisterHandle2);
78+
exposeCustomGlobal("_waitForActiveHandles", _waitForActiveHandles);
79+
exposeCustomGlobal("_getActiveHandles", _getActiveHandles);
80+
export { HANDLE_DISPATCH, _activeHandles, _waitResolvers, _registerHandle2, _unregisterHandle2, _waitForActiveHandles, _getActiveHandles };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Dependency-free-ish leaf for the buffer size caps. It imports ONLY from the
2+
// vendor leaves (esbuild-runtime + the vendored buffer), never from the cyclic
3+
// builtins, so `process.ts`/`module-loader.ts` can import these constants without
4+
// re-creating the import cycle that caused the earlier TDZ bug. The caps are read
5+
// dynamically from the vendored buffer module (exactly as the pre-split source did)
6+
// so a future buffer upgrade that exposes Buffer.kMaxLength/kStringMaxLength is
7+
// honored rather than silently overridden by a literal.
8+
import { __toESM } from "../vendor/esbuild-runtime.js";
9+
import { require_buffer } from "../vendor/buffer.js";
10+
11+
var import_buffer2 = __toESM(require_buffer(), 1);
12+
var BUFFER_MAX_LENGTH = typeof import_buffer2.Buffer.kMaxLength === "number" ? import_buffer2.Buffer.kMaxLength : 2147483647;
13+
var BUFFER_MAX_STRING_LENGTH = typeof import_buffer2.Buffer.kStringMaxLength === "number" ? import_buffer2.Buffer.kStringMaxLength : 536870888;
14+
var BUFFER_CONSTANTS = Object.freeze({
15+
MAX_LENGTH: BUFFER_MAX_LENGTH,
16+
MAX_STRING_LENGTH: BUFFER_MAX_STRING_LENGTH
17+
});
18+
19+
export { BUFFER_MAX_LENGTH, BUFFER_MAX_STRING_LENGTH, BUFFER_CONSTANTS };

0 commit comments

Comments
 (0)