Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions crates/execution/assets/runners/wasi-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -1281,11 +1281,17 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
const view = this._memoryView();
const offset = Number(statPtr) >>> 0;
const filetype = stats ? this._filetypeForStats(stats) : fallbackType;
const ino =
typeof stats?.inoExact === "bigint" ? stats.inoExact : BigInt(stats?.ino ?? 0);
const nlink =
typeof stats?.nlinkExact === "bigint" ? stats.nlinkExact : BigInt(stats?.nlink ?? 1);
const size =
typeof stats?.sizeExact === "bigint" ? stats.sizeExact : BigInt(stats?.size ?? 0);
view.setBigUint64(offset, 0n, true);
view.setBigUint64(offset + 8, BigInt(stats?.ino ?? 0), true);
view.setBigUint64(offset + 8, ino, true);
view.setUint8(offset + 16, filetype);
view.setBigUint64(offset + 24, BigInt(stats?.nlink ?? 1), true);
view.setBigUint64(offset + 32, BigInt(stats?.size ?? 0), true);
view.setBigUint64(offset + 24, nlink, true);
view.setBigUint64(offset + 32, size, true);
view.setBigUint64(offset + 40, BigInt(Math.trunc((stats?.atimeMs ?? 0) * 1000000)), true);
view.setBigUint64(offset + 48, BigInt(Math.trunc((stats?.mtimeMs ?? 0) * 1000000)), true);
view.setBigUint64(offset + 56, BigInt(Math.trunc((stats?.ctimeMs ?? 0) * 1000000)), true);
Expand Down Expand Up @@ -1484,10 +1490,22 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
}
}

_positionedIoOffset(offset) {
const explicitOffset = Number(offset);
if (!Number.isFinite(explicitOffset) || explicitOffset < 0) {
return null;
}
return explicitOffset;
}

_fdPwrite(fd, iovs, iovsLen, offset, nwrittenPtr) {
try {
const bytes = this._collectIovs(iovs, iovsLen);
const descriptor = Number(fd) >>> 0;
const explicitOffset = this._positionedIoOffset(offset);
if (explicitOffset === null) {
return __agentOSWasiErrnoInval;
}
const handle = this._externalFdHandle(descriptor);
if (
(handle?.kind === "passthrough" || handle?.kind === "host-passthrough") &&
Expand All @@ -1501,7 +1519,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
bytes,
0,
bytes.length,
Number(offset) >>> 0,
explicitOffset,
);
return this._writeUint32(nwrittenPtr, written);
}
Expand All @@ -1517,7 +1535,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
bytes,
0,
bytes.length,
Number(offset) >>> 0,
explicitOffset,
);
return this._writeUint32(nwrittenPtr, written);
} catch {
Expand All @@ -1528,7 +1546,10 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
_fdPread(fd, iovs, iovsLen, offset, nreadPtr) {
try {
const descriptor = Number(fd) >>> 0;
const explicitOffset = Number(offset) >>> 0;
const explicitOffset = this._positionedIoOffset(offset);
if (explicitOffset === null) {
return __agentOSWasiErrnoInval;
}
const totalLength = this._boundedReadLength(iovs, iovsLen);
const buffer = Buffer.alloc(totalLength);
const handle = this._externalFdHandle(descriptor);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/runtime-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export interface VirtualDirEntry {
export interface VirtualStat {
mode: number;
size: number;
sizeExact?: bigint;
blocks: number;
dev: number;
rdev: number;
Expand All @@ -118,7 +119,9 @@ export interface VirtualStat {
ctimeMs: number;
birthtimeMs: number;
ino: number;
inoExact?: bigint;
nlink: number;
nlinkExact?: bigint;
uid: number;
gid: number;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface VirtualDirEntry {
export interface VirtualStat {
mode: number;
size: number;
sizeExact?: bigint;
blocks: number;
dev: number;
rdev: number;
Expand All @@ -22,7 +23,9 @@ export interface VirtualStat {
ctimeMs: number;
birthtimeMs: number;
ino: number;
inoExact?: bigint;
nlink: number;
nlinkExact?: bigint;
uid: number;
gid: number;
}
Expand Down
22 changes: 15 additions & 7 deletions packages/core/src/sidecar/rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,9 @@ export class NativeSidecarKernelProxy {
options?.maxDepth,
);
}
return this.client.readdirRecursive(this.session, this.vm, path, options);
return (
await this.client.readdirRecursive(this.session, this.vm, path, options)
).map((entry) => ({ ...entry, size: Number(entry.size) }));
}

async removeFile(path: string): Promise<void> {
Expand Down Expand Up @@ -2620,21 +2622,27 @@ function errnoError(code: string, message: string): Error {
return Object.assign(new Error(`${code}: ${message}`), { code });
}

// VirtualStat is a numeric, Node-default-shaped view: u64 fields above
// Number.MAX_SAFE_INTEGER lose precision here, same as Node's non-bigint
// fs.stat on the host.
function toVirtualStat(stat: GuestFilesystemStat): VirtualStat {
return {
mode: stat.mode,
size: stat.size,
blocks: stat.blocks,
dev: stat.dev,
rdev: stat.rdev,
size: Number(stat.size),
sizeExact: stat.size,
blocks: Number(stat.blocks),
dev: Number(stat.dev),
rdev: Number(stat.rdev),
isDirectory: stat.is_directory,
isSymbolicLink: stat.is_symbolic_link,
atimeMs: stat.atime_ms,
mtimeMs: stat.mtime_ms,
ctimeMs: stat.ctime_ms,
birthtimeMs: stat.birthtime_ms,
ino: stat.ino,
nlink: stat.nlink,
ino: Number(stat.ino),
inoExact: stat.ino,
nlink: Number(stat.nlink),
nlinkExact: stat.nlink,
uid: stat.uid,
gid: stat.gid,
};
Expand Down
49 changes: 35 additions & 14 deletions packages/playground/agentos-worker.js

Large diffs are not rendered by default.

39 changes: 31 additions & 8 deletions packages/runtime-browser/src/converged-fs-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function convergedFilesystemSyncResponse(
case "fs.lstat":
return {
kind: SYNC_BRIDGE_KIND_JSON,
value: wireStatToVirtualStat(requireStat(result)),
value: virtualStatToSyncJson(wireStatToVirtualStat(requireStat(result))),
};
case "fs.writeFile":
case "fs.writeFileBinary":
Expand All @@ -251,27 +251,50 @@ export function convergedFilesystemSyncResponse(
}
}

/** Map a wire snake_case stat into the guest-facing camelCase `VirtualStat`. */
/**
* Map a wire snake_case stat into the guest-facing camelCase `VirtualStat`.
* The bigint u64 fields narrow to numbers here (lossy above
* Number.MAX_SAFE_INTEGER, matching Node's default fs.stat).
*/
export function wireStatToVirtualStat(stat: LiveGuestFilesystemStat): VirtualStat {
return {
mode: stat.mode,
size: stat.size,
blocks: stat.blocks,
dev: stat.dev,
rdev: stat.rdev,
size: Number(stat.size),
sizeExact: stat.size,
blocks: Number(stat.blocks),
dev: Number(stat.dev),
rdev: Number(stat.rdev),
isDirectory: stat.is_directory,
isSymbolicLink: stat.is_symbolic_link,
atimeMs: stat.atime_ms,
mtimeMs: stat.mtime_ms,
ctimeMs: stat.ctime_ms,
birthtimeMs: stat.birthtime_ms,
ino: stat.ino,
nlink: stat.nlink,
ino: Number(stat.ino),
inoExact: stat.ino,
nlink: Number(stat.nlink),
nlinkExact: stat.nlink,
uid: stat.uid,
gid: stat.gid,
};
}

/**
* Drop the optional `*Exact` bigint fields before a stat crosses the JSON
* sync bridge: `JSON.stringify` throws on bigint, and the guest-side Node
* shim only consumes the numeric fields anyway. In-process consumers (e.g.
* the WASI filestat writer) keep the exact fields.
*/
export function virtualStatToSyncJson(stat: VirtualStat): VirtualStat {
const {
sizeExact: _sizeExact,
inoExact: _inoExact,
nlinkExact: _nlinkExact,
...jsonStat
} = stat;
return jsonStat;
}

/** Build a `VirtualDirEntry` from a directory child name and its lstat. */
export function wireStatToDirEntry(
name: string,
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-browser/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface VirtualDirEntry {
export interface VirtualStat {
mode: number;
size: number;
sizeExact?: bigint;
blocks: number;
dev: number;
rdev: number;
Expand All @@ -32,7 +33,9 @@ export interface VirtualStat {
ctimeMs: number;
birthtimeMs: number;
ino: number;
inoExact?: bigint;
nlink: number;
nlinkExact?: bigint;
uid: number;
gid: number;
}
Expand Down
33 changes: 27 additions & 6 deletions packages/runtime-browser/src/wasi-polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,11 +1389,17 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
const view = this._memoryView();
const offset = Number(statPtr) >>> 0;
const filetype = stats ? this._filetypeForStats(stats) : fallbackType;
const ino =
typeof stats?.inoExact === "bigint" ? stats.inoExact : BigInt(stats?.ino ?? 0);
const nlink =
typeof stats?.nlinkExact === "bigint" ? stats.nlinkExact : BigInt(stats?.nlink ?? 1);
const size =
typeof stats?.sizeExact === "bigint" ? stats.sizeExact : BigInt(stats?.size ?? 0);
view.setBigUint64(offset, 0n, true);
view.setBigUint64(offset + 8, BigInt(stats?.ino ?? 0), true);
view.setBigUint64(offset + 8, ino, true);
view.setUint8(offset + 16, filetype);
view.setBigUint64(offset + 24, BigInt(stats?.nlink ?? 1), true);
view.setBigUint64(offset + 32, BigInt(stats?.size ?? 0), true);
view.setBigUint64(offset + 24, nlink, true);
view.setBigUint64(offset + 32, size, true);
view.setBigUint64(offset + 40, BigInt(Math.trunc((stats?.atimeMs ?? 0) * 1000000)), true);
view.setBigUint64(offset + 48, BigInt(Math.trunc((stats?.mtimeMs ?? 0) * 1000000)), true);
view.setBigUint64(offset + 56, BigInt(Math.trunc((stats?.ctimeMs ?? 0) * 1000000)), true);
Expand Down Expand Up @@ -1592,10 +1598,22 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
}
}

_positionedIoOffset(offset) {
const explicitOffset = Number(offset);
if (!Number.isFinite(explicitOffset) || explicitOffset < 0) {
return null;
}
return explicitOffset;
}

_fdPwrite(fd, iovs, iovsLen, offset, nwrittenPtr) {
try {
const bytes = this._collectIovs(iovs, iovsLen);
const descriptor = Number(fd) >>> 0;
const explicitOffset = this._positionedIoOffset(offset);
if (explicitOffset === null) {
return __agentOSWasiErrnoInval;
}
const handle = this._externalFdHandle(descriptor);
if (
(handle?.kind === "passthrough" || handle?.kind === "host-passthrough") &&
Expand All @@ -1609,7 +1627,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
bytes,
0,
bytes.length,
Number(offset) >>> 0,
explicitOffset,
);
return this._writeUint32(nwrittenPtr, written);
}
Expand All @@ -1625,7 +1643,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
bytes,
0,
bytes.length,
Number(offset) >>> 0,
explicitOffset,
);
return this._writeUint32(nwrittenPtr, written);
} catch {
Expand All @@ -1636,7 +1654,10 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
_fdPread(fd, iovs, iovsLen, offset, nreadPtr) {
try {
const descriptor = Number(fd) >>> 0;
const explicitOffset = Number(offset) >>> 0;
const explicitOffset = this._positionedIoOffset(offset);
if (explicitOffset === null) {
return __agentOSWasiErrnoInval;
}
const totalLength = this._boundedReadLength(iovs, iovsLen);
const buffer = Buffer.alloc(totalLength);
const handle = this._externalFdHandle(descriptor);
Expand Down
37 changes: 30 additions & 7 deletions packages/runtime-browser/tests/runtime/converged-fs-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
convergedFilesystemSyncResponse,
type GuestFilesystemResult,
isSingleCallFilesystemOperation,
virtualStatToSyncJson,
wireStatToDirEntry,
wireStatToVirtualStat,
} from "../../src/converged-fs-bridge.js";
Expand All @@ -17,18 +18,18 @@ import {

const SAMPLE_STAT: LiveGuestFilesystemStat = {
mode: 0o100644,
size: 12,
blocks: 1,
dev: 2,
rdev: 0,
size: 12n,
blocks: 1n,
dev: 2n,
rdev: 0n,
is_directory: false,
is_symbolic_link: false,
atime_ms: 10,
mtime_ms: 20,
ctime_ms: 30,
birthtime_ms: 40,
ino: 7,
nlink: 1,
ino: 7n,
nlink: 1n,
uid: 1000,
gid: 1000,
};
Expand Down Expand Up @@ -166,7 +167,7 @@ describe("converged filesystem bridge translation", () => {
);
expect(response).toEqual({
kind: SYNC_BRIDGE_KIND_JSON,
value: wireStatToVirtualStat(SAMPLE_STAT),
value: virtualStatToSyncJson(wireStatToVirtualStat(SAMPLE_STAT)),
});
expect(wireStatToVirtualStat(SAMPLE_STAT)).toMatchObject({
isDirectory: false,
Expand All @@ -176,6 +177,28 @@ describe("converged filesystem bridge translation", () => {
});
});

it("keeps stat sync responses JSON-serializable despite bigint exact fields", () => {
const unsafe = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
const response = convergedFilesystemSyncResponse(
"fs.stat",
result({
operation: "stat",
stat: { ...SAMPLE_STAT, size: unsafe, ino: unsafe, nlink: unsafe },
}),
);
if (response.kind !== SYNC_BRIDGE_KIND_JSON) {
throw new Error("expected JSON sync response");
}
// JSON.stringify throws on bigint; the sync bridge encodes with it.
const encoded = JSON.stringify(response.value);
const decoded = JSON.parse(encoded);
expect(decoded.ino).toBe(Number(unsafe));
expect(decoded.size).toBe(Number(unsafe));
expect(decoded).not.toHaveProperty("inoExact");
expect(decoded).not.toHaveProperty("sizeExact");
expect(decoded).not.toHaveProperty("nlinkExact");
});

it("returns NONE for mutations and JSON booleans for exists", () => {
expect(
convergedFilesystemSyncResponse(
Expand Down
Loading
Loading