@secure-exec/browser 0.0.0-agentos-dylib-base.edaa4a4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/child-process-bridge.d.ts +25 -0
- package/dist/child-process-bridge.js +50 -0
- package/dist/converged-base64.d.ts +2 -0
- package/dist/converged-base64.js +41 -0
- package/dist/converged-dgram-bridge.d.ts +11 -0
- package/dist/converged-dgram-bridge.js +147 -0
- package/dist/converged-driver-setup.d.ts +22 -0
- package/dist/converged-driver-setup.js +72 -0
- package/dist/converged-execution-host-bridge.d.ts +7 -0
- package/dist/converged-execution-host-bridge.js +85 -0
- package/dist/converged-executor-session.d.ts +60 -0
- package/dist/converged-executor-session.js +127 -0
- package/dist/converged-fs-bridge.d.ts +42 -0
- package/dist/converged-fs-bridge.js +245 -0
- package/dist/converged-module-servicer.d.ts +8 -0
- package/dist/converged-module-servicer.js +79 -0
- package/dist/converged-net-bridge.d.ts +28 -0
- package/dist/converged-net-bridge.js +155 -0
- package/dist/converged-permissions.d.ts +9 -0
- package/dist/converged-permissions.js +46 -0
- package/dist/converged-sync-bridge-handler.d.ts +47 -0
- package/dist/converged-sync-bridge-handler.js +140 -0
- package/dist/converged-sync-bridge-router.d.ts +33 -0
- package/dist/converged-sync-bridge-router.js +41 -0
- package/dist/driver.d.ts +91 -0
- package/dist/driver.js +386 -0
- package/dist/encoding.d.ts +4 -0
- package/dist/encoding.js +102 -0
- package/dist/generated/util-polyfill.d.ts +1 -0
- package/dist/generated/util-polyfill.js +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/kernel-backed-filesystem.d.ts +33 -0
- package/dist/kernel-backed-filesystem.js +205 -0
- package/dist/os-filesystem.d.ts +47 -0
- package/dist/os-filesystem.js +409 -0
- package/dist/permission-validation.d.ts +15 -0
- package/dist/permission-validation.js +62 -0
- package/dist/root-filesystem-from-vfs.d.ts +13 -0
- package/dist/root-filesystem-from-vfs.js +95 -0
- package/dist/runtime-driver.d.ts +66 -0
- package/dist/runtime-driver.js +611 -0
- package/dist/runtime.d.ts +248 -0
- package/dist/runtime.js +2296 -0
- package/dist/sidecar-wasm-module.d.ts +62 -0
- package/dist/sidecar-wasm-module.js +28 -0
- package/dist/sidecar-worker-protocol.d.ts +14 -0
- package/dist/sidecar-worker-protocol.js +9 -0
- package/dist/sidecar-worker.d.ts +19 -0
- package/dist/sidecar-worker.js +63 -0
- package/dist/signals.d.ts +13 -0
- package/dist/signals.js +89 -0
- package/dist/sync-bridge.d.ts +50 -0
- package/dist/sync-bridge.js +93 -0
- package/dist/wasi-polyfill.d.ts +1 -0
- package/dist/wasi-polyfill.js +2154 -0
- package/dist/worker-adapter.d.ts +21 -0
- package/dist/worker-adapter.js +41 -0
- package/dist/worker-protocol.d.ts +104 -0
- package/dist/worker-protocol.js +1 -0
- package/dist/worker-sidecar-client.d.ts +71 -0
- package/dist/worker-sidecar-client.js +152 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +2125 -0
- package/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type BrowserChildProcessBytes = {
|
|
2
|
+
__agentOsType: "bytes";
|
|
3
|
+
base64: string;
|
|
4
|
+
};
|
|
5
|
+
export type BrowserChildProcessSpawnOptions = {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
input?: BrowserChildProcessBytes | string | Uint8Array;
|
|
9
|
+
};
|
|
10
|
+
export type BrowserChildProcessSpawnRequest = {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
options?: BrowserChildProcessSpawnOptions;
|
|
14
|
+
};
|
|
15
|
+
export type BrowserChildProcessPollEvent = {
|
|
16
|
+
type: "stdout" | "stderr";
|
|
17
|
+
data: BrowserChildProcessBytes;
|
|
18
|
+
} | {
|
|
19
|
+
type: "exit";
|
|
20
|
+
exitCode: number;
|
|
21
|
+
signal: null;
|
|
22
|
+
};
|
|
23
|
+
export declare function encodeChildProcessBytes(data: Uint8Array): BrowserChildProcessBytes;
|
|
24
|
+
export declare function decodeChildProcessInput(value: unknown): Uint8Array | undefined;
|
|
25
|
+
export declare function parseChildProcessSpawnRequest(value: unknown, label: string): BrowserChildProcessSpawnRequest;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64, toUint8Array } from "./encoding.js";
|
|
2
|
+
export function encodeChildProcessBytes(data) {
|
|
3
|
+
return {
|
|
4
|
+
__agentOsType: "bytes",
|
|
5
|
+
base64: bytesToBase64(data),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function decodeChildProcessInput(value) {
|
|
9
|
+
if (value == null) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "object" &&
|
|
13
|
+
value.__agentOsType === "bytes" &&
|
|
14
|
+
typeof value.base64 === "string") {
|
|
15
|
+
return base64ToBytes(value.base64);
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
return new TextEncoder().encode(value);
|
|
19
|
+
}
|
|
20
|
+
return toUint8Array(value);
|
|
21
|
+
}
|
|
22
|
+
export function parseChildProcessSpawnRequest(value, label) {
|
|
23
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
24
|
+
if (!parsed || typeof parsed !== "object") {
|
|
25
|
+
throw new Error(`${label} must be an object`);
|
|
26
|
+
}
|
|
27
|
+
const record = parsed;
|
|
28
|
+
if (typeof record.command !== "string") {
|
|
29
|
+
throw new Error(`${label}.command must be a string`);
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(record.args)) {
|
|
32
|
+
throw new Error(`${label}.args must be an array`);
|
|
33
|
+
}
|
|
34
|
+
const rawOptions = record.options;
|
|
35
|
+
const optionsRecord = rawOptions && typeof rawOptions === "object"
|
|
36
|
+
? rawOptions
|
|
37
|
+
: {};
|
|
38
|
+
const env = optionsRecord.env && typeof optionsRecord.env === "object"
|
|
39
|
+
? Object.fromEntries(Object.entries(optionsRecord.env).map(([key, envValue]) => [key, String(envValue)]))
|
|
40
|
+
: undefined;
|
|
41
|
+
return {
|
|
42
|
+
command: record.command,
|
|
43
|
+
args: record.args.map((entry) => String(entry)),
|
|
44
|
+
options: {
|
|
45
|
+
cwd: typeof optionsRecord.cwd === "string" ? optionsRecord.cwd : undefined,
|
|
46
|
+
env,
|
|
47
|
+
input: optionsRecord.input,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Standard base64 codec over byte arrays, shared by the converged bridge
|
|
2
|
+
// translation layers. Self-contained (no `atob`/`btoa`) so it behaves
|
|
3
|
+
// identically in a Worker, the main thread, and Node/vitest, and is binary-safe
|
|
4
|
+
// (atob/btoa are latin1-only).
|
|
5
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
6
|
+
export function encodeBase64(bytes) {
|
|
7
|
+
let output = "";
|
|
8
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
9
|
+
const byte0 = bytes[index];
|
|
10
|
+
const byte1 = bytes[index + 1];
|
|
11
|
+
const byte2 = bytes[index + 2];
|
|
12
|
+
const triple = (byte0 << 16) | ((byte1 ?? 0) << 8) | (byte2 ?? 0);
|
|
13
|
+
output += BASE64_ALPHABET[(triple >> 18) & 0x3f];
|
|
14
|
+
output += BASE64_ALPHABET[(triple >> 12) & 0x3f];
|
|
15
|
+
output +=
|
|
16
|
+
index + 1 < bytes.length ? BASE64_ALPHABET[(triple >> 6) & 0x3f] : "=";
|
|
17
|
+
output +=
|
|
18
|
+
index + 2 < bytes.length ? BASE64_ALPHABET[triple & 0x3f] : "=";
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
export function decodeBase64(input) {
|
|
23
|
+
const clean = input.replace(/[^A-Za-z0-9+/]/g, "");
|
|
24
|
+
const length = Math.floor((clean.length * 3) / 4);
|
|
25
|
+
const bytes = new Uint8Array(length);
|
|
26
|
+
let outIndex = 0;
|
|
27
|
+
for (let index = 0; index < clean.length; index += 4) {
|
|
28
|
+
const enc0 = BASE64_ALPHABET.indexOf(clean[index]);
|
|
29
|
+
const enc1 = BASE64_ALPHABET.indexOf(clean[index + 1]);
|
|
30
|
+
const enc2 = BASE64_ALPHABET.indexOf(clean[index + 2]);
|
|
31
|
+
const enc3 = BASE64_ALPHABET.indexOf(clean[index + 3]);
|
|
32
|
+
const triple = (enc0 << 18) | (enc1 << 12) | ((enc2 & 0x3f) << 6) | (enc3 & 0x3f);
|
|
33
|
+
if (outIndex < length)
|
|
34
|
+
bytes[outIndex++] = (triple >> 16) & 0xff;
|
|
35
|
+
if (enc2 !== -1 && outIndex < length)
|
|
36
|
+
bytes[outIndex++] = (triple >> 8) & 0xff;
|
|
37
|
+
if (enc3 !== -1 && outIndex < length)
|
|
38
|
+
bytes[outIndex++] = triple & 0xff;
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConvergedSyncResponse } from "./converged-fs-bridge.js";
|
|
2
|
+
import type { GuestKernelCallRequestPayload } from "./converged-net-bridge.js";
|
|
3
|
+
export declare function isConvergedDgramBridgeOperation(operation: string): boolean;
|
|
4
|
+
/** True if `operation` needs a wasm kernel round-trip (vs an inline response). */
|
|
5
|
+
export declare function dgramOperationUsesKernel(operation: string): boolean;
|
|
6
|
+
/** Inline response for buffer-size ops (no kernel counterpart; advisory). */
|
|
7
|
+
export declare function convergedDgramInlineResponse(operation: string): ConvergedSyncResponse;
|
|
8
|
+
/** Translate a guest `dgram.*` sync op + positional args into a kernel call. */
|
|
9
|
+
export declare function convergedDgramRequestPayload(operation: string, args: readonly unknown[], executionId: string): GuestKernelCallRequestPayload;
|
|
10
|
+
/** Map a kernel `dgram.*` JSON result back into the guest's expected shape. */
|
|
11
|
+
export declare function convergedDgramSyncResponse(operation: string, result: unknown): ConvergedSyncResponse;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Converged dgram (UDP) bridge translation layer.
|
|
2
|
+
//
|
|
3
|
+
// The guest worker already issues `dgram.*` sync-bridge ops (positional args).
|
|
4
|
+
// This routes them to the wasm kernel UDP socket table via `guest_kernel_call`
|
|
5
|
+
// (`dgram.*` ops in `sidecar-core::guest_net`) instead of the legacy in-process
|
|
6
|
+
// TS dgram sessions, and maps the kernel responses back to the exact shapes the
|
|
7
|
+
// guest dgram module consumes (mirroring the legacy servicer). Buffer-size ops
|
|
8
|
+
// have no kernel counterpart and are handled inline (advisory).
|
|
9
|
+
import { encodeBase64 } from "./converged-base64.js";
|
|
10
|
+
import { SYNC_BRIDGE_KIND_JSON } from "./sync-bridge.js";
|
|
11
|
+
const KERNEL_DGRAM_OPERATIONS = new Set([
|
|
12
|
+
"dgram.create",
|
|
13
|
+
"dgram.bind",
|
|
14
|
+
"dgram.recv",
|
|
15
|
+
"dgram.send",
|
|
16
|
+
"dgram.close",
|
|
17
|
+
"dgram.address",
|
|
18
|
+
]);
|
|
19
|
+
const BUFFER_SIZE_OPERATIONS = new Set([
|
|
20
|
+
"dgram.setBufferSize",
|
|
21
|
+
"dgram.getBufferSize",
|
|
22
|
+
]);
|
|
23
|
+
const DEFAULT_BUFFER_SIZE = 65536;
|
|
24
|
+
export function isConvergedDgramBridgeOperation(operation) {
|
|
25
|
+
return (KERNEL_DGRAM_OPERATIONS.has(operation) ||
|
|
26
|
+
BUFFER_SIZE_OPERATIONS.has(operation));
|
|
27
|
+
}
|
|
28
|
+
/** True if `operation` needs a wasm kernel round-trip (vs an inline response). */
|
|
29
|
+
export function dgramOperationUsesKernel(operation) {
|
|
30
|
+
return KERNEL_DGRAM_OPERATIONS.has(operation);
|
|
31
|
+
}
|
|
32
|
+
/** Inline response for buffer-size ops (no kernel counterpart; advisory). */
|
|
33
|
+
export function convergedDgramInlineResponse(operation) {
|
|
34
|
+
if (operation === "dgram.getBufferSize") {
|
|
35
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { size: DEFAULT_BUFFER_SIZE } };
|
|
36
|
+
}
|
|
37
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { ok: true } };
|
|
38
|
+
}
|
|
39
|
+
/** Translate a guest `dgram.*` sync op + positional args into a kernel call. */
|
|
40
|
+
export function convergedDgramRequestPayload(operation, args, executionId) {
|
|
41
|
+
const request = buildKernelRequest(operation, args);
|
|
42
|
+
return {
|
|
43
|
+
type: "guest_kernel_call",
|
|
44
|
+
execution_id: executionId,
|
|
45
|
+
operation,
|
|
46
|
+
payload: encodeJsonBytes(request),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Map a kernel `dgram.*` JSON result back into the guest's expected shape. */
|
|
50
|
+
export function convergedDgramSyncResponse(operation, result) {
|
|
51
|
+
const value = (result ?? {});
|
|
52
|
+
switch (operation) {
|
|
53
|
+
case "dgram.create":
|
|
54
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { socketId: value.socketId } };
|
|
55
|
+
case "dgram.bind":
|
|
56
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { ok: true } };
|
|
57
|
+
case "dgram.close":
|
|
58
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { ok: true } };
|
|
59
|
+
case "dgram.send":
|
|
60
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: { bytes: value.bytes ?? 0 } };
|
|
61
|
+
case "dgram.recv": {
|
|
62
|
+
if (value.data === null || value.data === undefined) {
|
|
63
|
+
return { kind: SYNC_BRIDGE_KIND_JSON, value: null };
|
|
64
|
+
}
|
|
65
|
+
const remoteAddress = String(value.remoteAddress ?? "");
|
|
66
|
+
return {
|
|
67
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
68
|
+
value: {
|
|
69
|
+
type: "message",
|
|
70
|
+
data: value.data,
|
|
71
|
+
remoteAddress,
|
|
72
|
+
remotePort: value.remotePort,
|
|
73
|
+
remoteFamily: remoteAddress.includes(":") ? "IPv6" : "IPv4",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "dgram.address": {
|
|
78
|
+
if (value.host === null || value.host === undefined) {
|
|
79
|
+
const error = new Error("getsockname EBADF");
|
|
80
|
+
error.code = "EBADF";
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const address = String(value.host);
|
|
84
|
+
return {
|
|
85
|
+
kind: SYNC_BRIDGE_KIND_JSON,
|
|
86
|
+
value: {
|
|
87
|
+
address,
|
|
88
|
+
port: value.port,
|
|
89
|
+
family: address.includes(":") ? "IPv6" : "IPv4",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`converged dgram bridge has no response for ${operation}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function buildKernelRequest(operation, args) {
|
|
98
|
+
switch (operation) {
|
|
99
|
+
case "dgram.create":
|
|
100
|
+
return {};
|
|
101
|
+
case "dgram.bind": {
|
|
102
|
+
const options = (args[1] ?? {});
|
|
103
|
+
return {
|
|
104
|
+
socketId: requireSocketId(args[0]),
|
|
105
|
+
host: typeof options.address === "string" ? options.address : "127.0.0.1",
|
|
106
|
+
port: typeof options.port === "number" ? options.port : 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case "dgram.recv":
|
|
110
|
+
return { socketId: requireSocketId(args[0]) };
|
|
111
|
+
case "dgram.send": {
|
|
112
|
+
const target = (args[2] ?? {});
|
|
113
|
+
return {
|
|
114
|
+
socketId: requireSocketId(args[0]),
|
|
115
|
+
host: typeof target.address === "string" ? target.address : "127.0.0.1",
|
|
116
|
+
port: typeof target.port === "number" ? target.port : 0,
|
|
117
|
+
data: encodeBase64(toUint8Array(args[1])),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case "dgram.close":
|
|
121
|
+
case "dgram.address":
|
|
122
|
+
return { socketId: requireSocketId(args[0]) };
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(`converged dgram bridge has no mapping for ${operation}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function requireSocketId(value) {
|
|
128
|
+
const socketId = typeof value === "string" ? Number(value) : value;
|
|
129
|
+
if (typeof socketId !== "number" || Number.isNaN(socketId)) {
|
|
130
|
+
throw new Error("converged dgram bridge call requires a numeric socketId");
|
|
131
|
+
}
|
|
132
|
+
return socketId;
|
|
133
|
+
}
|
|
134
|
+
function toUint8Array(value) {
|
|
135
|
+
if (value instanceof Uint8Array)
|
|
136
|
+
return value;
|
|
137
|
+
if (value instanceof ArrayBuffer)
|
|
138
|
+
return new Uint8Array(value);
|
|
139
|
+
if (ArrayBuffer.isView(value)) {
|
|
140
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
141
|
+
}
|
|
142
|
+
throw new Error("converged dgram send requires binary data");
|
|
143
|
+
}
|
|
144
|
+
function encodeJsonBytes(value) {
|
|
145
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
146
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CreateVmConfig } from "@secure-exec/core/vm-config";
|
|
2
|
+
import type { ProtocolFramePayloadCodec } from "@secure-exec/core/protocol-frames";
|
|
3
|
+
import type { ConvergedSyncResponse } from "./converged-fs-bridge.js";
|
|
4
|
+
import { ConvergedExecutorSession } from "./converged-executor-session.js";
|
|
5
|
+
import { type LegacySyncBridgeServicer } from "./converged-sync-bridge-router.js";
|
|
6
|
+
export interface ConvergedServicerOptions {
|
|
7
|
+
pushFrame: (frame: Uint8Array) => Uint8Array;
|
|
8
|
+
config: CreateVmConfig;
|
|
9
|
+
codec?: ProtocolFramePayloadCodec;
|
|
10
|
+
setNextExecutionId?: (executionId: string) => void;
|
|
11
|
+
onFsReadDenied?: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ConvergedServicer {
|
|
14
|
+
/**
|
|
15
|
+
* Service one guest sync-bridge operation: fs/net/dns to the wasm handler,
|
|
16
|
+
* module.* to the kernel-backed resolver, everything else to `legacy`.
|
|
17
|
+
*/
|
|
18
|
+
route(executionId: string, operation: string, args: readonly unknown[], legacy: LegacySyncBridgeServicer): Promise<ConvergedSyncResponse>;
|
|
19
|
+
/** Snapshot the VM root filesystem (for host persistence, e.g. OPFS). */
|
|
20
|
+
snapshotRootFilesystem(): ReturnType<ConvergedExecutorSession["snapshotRootFilesystem"]>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createConvergedServicer(options: ConvergedServicerOptions): ConvergedServicer;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Converged driver servicer assembly.
|
|
2
|
+
//
|
|
3
|
+
// Bundles the converged executor pieces (session bootstrap, fs/net/dns handler,
|
|
4
|
+
// kernel-backed module resolver, router) into a single servicer the runtime
|
|
5
|
+
// driver uses to satisfy a guest's sync-bridge requests against the wasm kernel.
|
|
6
|
+
//
|
|
7
|
+
// The runtime driver loads this DYNAMICALLY (only when a converged sidecar is
|
|
8
|
+
// configured) so the legacy, unbundled `/dist/` driver load never pulls in the
|
|
9
|
+
// bare `@secure-exec/core/*` imports these modules need — those resolve only
|
|
10
|
+
// when the converged runtime is esbuild-bundled.
|
|
11
|
+
import { isConvergedDgramBridgeOperation } from "./converged-dgram-bridge.js";
|
|
12
|
+
import { ConvergedExecutorSession } from "./converged-executor-session.js";
|
|
13
|
+
import { ConvergedModuleServicer } from "./converged-module-servicer.js";
|
|
14
|
+
import { isConvergedNetBridgeOperation } from "./converged-net-bridge.js";
|
|
15
|
+
import { ConvergedSyncBridgeRouter, } from "./converged-sync-bridge-router.js";
|
|
16
|
+
import { KernelBackedFilesystem } from "./kernel-backed-filesystem.js";
|
|
17
|
+
const FS_READ_OPERATIONS = new Set([
|
|
18
|
+
"fs.readFile",
|
|
19
|
+
"fs.readFileBinary",
|
|
20
|
+
"fs.readDir",
|
|
21
|
+
"fs.stat",
|
|
22
|
+
"fs.lstat",
|
|
23
|
+
"fs.exists",
|
|
24
|
+
"fs.realpath",
|
|
25
|
+
"fs.readlink",
|
|
26
|
+
]);
|
|
27
|
+
export function createConvergedServicer(options) {
|
|
28
|
+
const session = new ConvergedExecutorSession({
|
|
29
|
+
pushFrame: options.pushFrame,
|
|
30
|
+
codec: options.codec,
|
|
31
|
+
});
|
|
32
|
+
session.bootstrap({ runtime: "java_script", config: options.config });
|
|
33
|
+
const moduleServicer = new ConvergedModuleServicer(new KernelBackedFilesystem(session.transportForVm()));
|
|
34
|
+
const registeredExecutions = new Set();
|
|
35
|
+
const ensureExecutionRegistered = (executionId) => {
|
|
36
|
+
if (registeredExecutions.has(executionId) || !options.setNextExecutionId) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
options.setNextExecutionId(executionId);
|
|
40
|
+
session.registerExecution({ processId: executionId, args: ["node"] });
|
|
41
|
+
registeredExecutions.add(executionId);
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
async route(executionId, operation, args, legacy) {
|
|
45
|
+
// Guest net/dgram need a kernel process (pid); register it lazily on
|
|
46
|
+
// first use so the guest_kernel_call resolves execution_id -> pid.
|
|
47
|
+
if (isConvergedNetBridgeOperation(operation) ||
|
|
48
|
+
isConvergedDgramBridgeOperation(operation)) {
|
|
49
|
+
ensureExecutionRegistered(executionId);
|
|
50
|
+
}
|
|
51
|
+
const router = new ConvergedSyncBridgeRouter({
|
|
52
|
+
handler: session.handlerForExecution(executionId),
|
|
53
|
+
asyncServicers: [moduleServicer],
|
|
54
|
+
legacy,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
return await router.route(operation, args);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (options.onFsReadDenied &&
|
|
61
|
+
FS_READ_OPERATIONS.has(operation) &&
|
|
62
|
+
error?.code === "EACCES") {
|
|
63
|
+
options.onFsReadDenied();
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
snapshotRootFilesystem() {
|
|
69
|
+
return session.snapshotRootFilesystem();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ConvergedExecutionHostBridge {
|
|
2
|
+
/** Set the execution id `startExecution` will echo for the next `execute`. */
|
|
3
|
+
setNextExecutionId(executionId: string): void;
|
|
4
|
+
/** The host-bridge object passed to `new BrowserSidecarWasm(bridge)`. */
|
|
5
|
+
readonly bridge: Record<string, (requestJson: string) => unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createConvergedExecutionHostBridge(): ConvergedExecutionHostBridge;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Execution host bridge for the converged wasm sidecar.
|
|
2
|
+
//
|
|
3
|
+
// In the converged browser runtime the guest runs in the browser worker, not in
|
|
4
|
+
// the wasm sidecar. But guest `net.*`/`dgram.*` syscalls need a kernel process
|
|
5
|
+
// (pid) and socket ownership inside the sidecar, which is created by an `execute`
|
|
6
|
+
// wire request. That request drives these host-bridge callbacks. They are
|
|
7
|
+
// no-ops that just satisfy the sidecar's execution lifecycle: the only one that
|
|
8
|
+
// matters is `startExecution`, which echoes the driver-provided execution id so
|
|
9
|
+
// the sidecar registers the kernel process under the SAME id the guest worker
|
|
10
|
+
// uses in its sync-bridge `guest_kernel_call`s.
|
|
11
|
+
//
|
|
12
|
+
// The wasm `BrowserJsBridge` invokes each method with a JSON request string and
|
|
13
|
+
// JSON-decodes the return value.
|
|
14
|
+
export function createConvergedExecutionHostBridge() {
|
|
15
|
+
let nextExecutionId = "converged-exec";
|
|
16
|
+
let contextCounter = 0;
|
|
17
|
+
let workerCounter = 0;
|
|
18
|
+
const bridge = {
|
|
19
|
+
createJavascriptContext() {
|
|
20
|
+
contextCounter += 1;
|
|
21
|
+
return { contextId: `converged-ctx-${contextCounter}` };
|
|
22
|
+
},
|
|
23
|
+
createWasmContext() {
|
|
24
|
+
contextCounter += 1;
|
|
25
|
+
return { contextId: `converged-wasm-ctx-${contextCounter}` };
|
|
26
|
+
},
|
|
27
|
+
startExecution() {
|
|
28
|
+
return { executionId: nextExecutionId };
|
|
29
|
+
},
|
|
30
|
+
createWorker(requestJson) {
|
|
31
|
+
workerCounter += 1;
|
|
32
|
+
const request = parse(requestJson);
|
|
33
|
+
return {
|
|
34
|
+
workerId: `converged-worker-${workerCounter}`,
|
|
35
|
+
runtime: typeof request.runtime === "string" ? request.runtime : undefined,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
writeExecutionStdin() {
|
|
39
|
+
return {};
|
|
40
|
+
},
|
|
41
|
+
closeExecutionStdin() {
|
|
42
|
+
return {};
|
|
43
|
+
},
|
|
44
|
+
killExecution() {
|
|
45
|
+
return {};
|
|
46
|
+
},
|
|
47
|
+
pollExecutionEvent() {
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
terminateWorker() {
|
|
51
|
+
return {};
|
|
52
|
+
},
|
|
53
|
+
// Diagnostics/observability callbacks the execute lifecycle emits; no-ops
|
|
54
|
+
// here (the converged driver surfaces events through its own channels).
|
|
55
|
+
emitStructuredEvent() {
|
|
56
|
+
return {};
|
|
57
|
+
},
|
|
58
|
+
emitDiagnostic() {
|
|
59
|
+
return {};
|
|
60
|
+
},
|
|
61
|
+
emitLog() {
|
|
62
|
+
return {};
|
|
63
|
+
},
|
|
64
|
+
emitLifecycle() {
|
|
65
|
+
return {};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
setNextExecutionId(executionId) {
|
|
70
|
+
nextExecutionId = executionId;
|
|
71
|
+
},
|
|
72
|
+
bridge,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parse(requestJson) {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(requestJson);
|
|
78
|
+
return typeof value === "object" && value !== null
|
|
79
|
+
? value
|
|
80
|
+
: {};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CreateVmConfig } from "@secure-exec/core/vm-config";
|
|
2
|
+
import type { LiveRootFilesystemEntry as RootFilesystemEntry } from "@secure-exec/core/filesystem";
|
|
3
|
+
import type { ProtocolFramePayloadCodec } from "@secure-exec/core/protocol-frames";
|
|
4
|
+
import type { LiveRequestPayload } from "@secure-exec/core/request-payloads";
|
|
5
|
+
import { ConvergedSyncBridgeHandler, type ConvergedPushFrame, PushFrameSidecarTransport } from "./converged-sync-bridge-handler.js";
|
|
6
|
+
type GuestRuntimeKind = Extract<LiveRequestPayload, {
|
|
7
|
+
type: "create_vm";
|
|
8
|
+
}>["runtime"];
|
|
9
|
+
export interface ConvergedExecutorSessionOptions {
|
|
10
|
+
pushFrame: ConvergedPushFrame;
|
|
11
|
+
codec?: ProtocolFramePayloadCodec;
|
|
12
|
+
}
|
|
13
|
+
export interface ConvergedVmBootstrap {
|
|
14
|
+
runtime: GuestRuntimeKind;
|
|
15
|
+
config: CreateVmConfig;
|
|
16
|
+
sessionMetadata?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/** A bootstrapped VM inside the wasm sidecar. */
|
|
19
|
+
export interface ConvergedVm {
|
|
20
|
+
connectionId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
vmId: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class ConvergedExecutorSession {
|
|
25
|
+
private readonly pushFrame;
|
|
26
|
+
private readonly codec;
|
|
27
|
+
private vm;
|
|
28
|
+
constructor(options: ConvergedExecutorSessionOptions);
|
|
29
|
+
/** The bootstrapped VM, or throw if `bootstrap()` has not run. */
|
|
30
|
+
get currentVm(): ConvergedVm;
|
|
31
|
+
/** Run the authenticate -> open_session -> create_vm handshake. */
|
|
32
|
+
bootstrap(options: ConvergedVmBootstrap): ConvergedVm;
|
|
33
|
+
/** A synchronous syscall handler scoped to the bootstrapped VM + execution. */
|
|
34
|
+
handlerForExecution(executionId: string): ConvergedSyncBridgeHandler;
|
|
35
|
+
/**
|
|
36
|
+
* Register a guest execution (kernel process) in the sidecar via an `execute`
|
|
37
|
+
* wire request, so guest `net.*`/`dgram.*` syscalls can resolve their
|
|
38
|
+
* `execution_id` to a kernel pid. The guest itself runs in the browser worker;
|
|
39
|
+
* this only owns the kernel-side process/socket lifecycle. Requires the wasm
|
|
40
|
+
* sidecar to be constructed with an execution host bridge whose
|
|
41
|
+
* `startExecution` echoes `processId` back as the execution id.
|
|
42
|
+
*/
|
|
43
|
+
registerExecution(options: {
|
|
44
|
+
processId: string;
|
|
45
|
+
entrypoint?: string;
|
|
46
|
+
args?: readonly string[];
|
|
47
|
+
cwd?: string;
|
|
48
|
+
}): {
|
|
49
|
+
processId: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot the VM's root filesystem (the writable changes) so callers can
|
|
53
|
+
* persist them to host storage (e.g. OPFS) across runtimes.
|
|
54
|
+
*/
|
|
55
|
+
snapshotRootFilesystem(): RootFilesystemEntry[];
|
|
56
|
+
/** A request transport bound to the bootstrapped VM ownership. */
|
|
57
|
+
transportForVm(): PushFrameSidecarTransport;
|
|
58
|
+
private send;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Converged executor session bootstrap.
|
|
2
|
+
//
|
|
3
|
+
// Brings up a VM inside the wasm sidecar over the synchronous `pushFrame`
|
|
4
|
+
// dispatcher (authenticate -> open_session -> create_vm), then hands out a
|
|
5
|
+
// per-execution `ConvergedSyncBridgeHandler` bound to that VM's ownership. This
|
|
6
|
+
// is the glue between the wasm sidecar and the guest Worker's sync-bridge: the
|
|
7
|
+
// handshake runs once at setup, and each guest execution gets a synchronous
|
|
8
|
+
// handler that routes its syscalls to the kernel.
|
|
9
|
+
//
|
|
10
|
+
// The handshake uses the same client identity and versions as the core
|
|
11
|
+
// `SidecarProcess` so the wasm sidecar accepts it. Unit-tested with a fake
|
|
12
|
+
// synchronous `pushFrame`.
|
|
13
|
+
import { SIDECAR_PROTOCOL_SCHEMA } from "@secure-exec/core/protocol-schema";
|
|
14
|
+
import { ConvergedSyncBridgeHandler, PushFrameSidecarTransport, } from "./converged-sync-bridge-handler.js";
|
|
15
|
+
// Mirror `SidecarProcess`'s client identity so the sidecar handshake succeeds.
|
|
16
|
+
const CLIENT_NAME = "secure-exec-core-client";
|
|
17
|
+
const AUTH_TOKEN = "secure-exec-core-client-token";
|
|
18
|
+
const BRIDGE_CONTRACT_VERSION = 1;
|
|
19
|
+
export class ConvergedExecutorSession {
|
|
20
|
+
pushFrame;
|
|
21
|
+
codec;
|
|
22
|
+
vm;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.pushFrame = options.pushFrame;
|
|
25
|
+
this.codec = options.codec ?? "bare";
|
|
26
|
+
}
|
|
27
|
+
/** The bootstrapped VM, or throw if `bootstrap()` has not run. */
|
|
28
|
+
get currentVm() {
|
|
29
|
+
if (!this.vm) {
|
|
30
|
+
throw new Error("converged executor session has not bootstrapped a VM");
|
|
31
|
+
}
|
|
32
|
+
return this.vm;
|
|
33
|
+
}
|
|
34
|
+
/** Run the authenticate -> open_session -> create_vm handshake. */
|
|
35
|
+
bootstrap(options) {
|
|
36
|
+
const authenticated = this.send({ scope: "connection", connection_id: "client-hint" }, {
|
|
37
|
+
type: "authenticate",
|
|
38
|
+
client_name: CLIENT_NAME,
|
|
39
|
+
auth_token: AUTH_TOKEN,
|
|
40
|
+
protocol_version: SIDECAR_PROTOCOL_SCHEMA.version,
|
|
41
|
+
bridge_version: BRIDGE_CONTRACT_VERSION,
|
|
42
|
+
});
|
|
43
|
+
if (authenticated.type !== "authenticated") {
|
|
44
|
+
throw new Error(`unexpected authenticate response: ${authenticated.type}`);
|
|
45
|
+
}
|
|
46
|
+
const connectionId = authenticated.connection_id;
|
|
47
|
+
const opened = this.send({ scope: "connection", connection_id: connectionId }, {
|
|
48
|
+
type: "open_session",
|
|
49
|
+
placement: { kind: "shared", pool: null },
|
|
50
|
+
metadata: options.sessionMetadata ?? {},
|
|
51
|
+
});
|
|
52
|
+
if (opened.type !== "session_opened") {
|
|
53
|
+
throw new Error(`unexpected open_session response: ${opened.type}`);
|
|
54
|
+
}
|
|
55
|
+
const sessionId = opened.session_id;
|
|
56
|
+
const created = this.send({ scope: "session", connection_id: connectionId, session_id: sessionId }, { type: "create_vm", runtime: options.runtime, config: options.config });
|
|
57
|
+
if (created.type !== "vm_created") {
|
|
58
|
+
throw new Error(`unexpected create_vm response: ${created.type}`);
|
|
59
|
+
}
|
|
60
|
+
this.vm = { connectionId, sessionId, vmId: created.vm_id };
|
|
61
|
+
return this.vm;
|
|
62
|
+
}
|
|
63
|
+
/** A synchronous syscall handler scoped to the bootstrapped VM + execution. */
|
|
64
|
+
handlerForExecution(executionId) {
|
|
65
|
+
return new ConvergedSyncBridgeHandler({
|
|
66
|
+
transport: this.transportForVm(),
|
|
67
|
+
executionId,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register a guest execution (kernel process) in the sidecar via an `execute`
|
|
72
|
+
* wire request, so guest `net.*`/`dgram.*` syscalls can resolve their
|
|
73
|
+
* `execution_id` to a kernel pid. The guest itself runs in the browser worker;
|
|
74
|
+
* this only owns the kernel-side process/socket lifecycle. Requires the wasm
|
|
75
|
+
* sidecar to be constructed with an execution host bridge whose
|
|
76
|
+
* `startExecution` echoes `processId` back as the execution id.
|
|
77
|
+
*/
|
|
78
|
+
registerExecution(options) {
|
|
79
|
+
const response = this.transportForVm().sendRequest({
|
|
80
|
+
type: "execute",
|
|
81
|
+
process_id: options.processId,
|
|
82
|
+
runtime: "java_script",
|
|
83
|
+
entrypoint: options.entrypoint,
|
|
84
|
+
args: [...(options.args ?? [])],
|
|
85
|
+
cwd: options.cwd,
|
|
86
|
+
});
|
|
87
|
+
if (response.type !== "process_started") {
|
|
88
|
+
throw new Error(`unexpected execute response: ${response.type}`);
|
|
89
|
+
}
|
|
90
|
+
return { processId: response.process_id };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Snapshot the VM's root filesystem (the writable changes) so callers can
|
|
94
|
+
* persist them to host storage (e.g. OPFS) across runtimes.
|
|
95
|
+
*/
|
|
96
|
+
snapshotRootFilesystem() {
|
|
97
|
+
const response = this.transportForVm().sendRequest({
|
|
98
|
+
type: "snapshot_root_filesystem",
|
|
99
|
+
});
|
|
100
|
+
if (response.type !== "root_filesystem_snapshot") {
|
|
101
|
+
throw new Error(`unexpected snapshot_root_filesystem response: ${response.type}`);
|
|
102
|
+
}
|
|
103
|
+
return response.entries;
|
|
104
|
+
}
|
|
105
|
+
/** A request transport bound to the bootstrapped VM ownership. */
|
|
106
|
+
transportForVm() {
|
|
107
|
+
const vm = this.currentVm;
|
|
108
|
+
return new PushFrameSidecarTransport({
|
|
109
|
+
pushFrame: this.pushFrame,
|
|
110
|
+
codec: this.codec,
|
|
111
|
+
ownership: {
|
|
112
|
+
scope: "vm",
|
|
113
|
+
connection_id: vm.connectionId,
|
|
114
|
+
session_id: vm.sessionId,
|
|
115
|
+
vm_id: vm.vmId,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
send(ownership, payload) {
|
|
120
|
+
const transport = new PushFrameSidecarTransport({
|
|
121
|
+
pushFrame: this.pushFrame,
|
|
122
|
+
codec: this.codec,
|
|
123
|
+
ownership,
|
|
124
|
+
});
|
|
125
|
+
return transport.sendRequest(payload);
|
|
126
|
+
}
|
|
127
|
+
}
|