@forgeax/engine-remote 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +269 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/cli-defaults.unit.test.d.ts +2 -0
- package/dist/__tests__/cli-defaults.unit.test.d.ts.map +1 -0
- package/dist/__tests__/console.unit.test.d.ts +2 -0
- package/dist/__tests__/console.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execute-browser-safe.unit.test.d.ts +2 -0
- package/dist/__tests__/execute-browser-safe.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execute-profiler-root.browser.test.d.ts +2 -0
- package/dist/__tests__/execute-profiler-root.browser.test.d.ts.map +1 -0
- package/dist/__tests__/execute.async.test.d.ts +2 -0
- package/dist/__tests__/execute.async.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report-root.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-report-root.unit.test.d.ts.map +1 -0
- package/dist/__tests__/introspect-profiler.unit.test.d.ts +2 -0
- package/dist/__tests__/introspect-profiler.unit.test.d.ts.map +1 -0
- package/dist/__tests__/method-roster-profiler.test.d.ts +2 -0
- package/dist/__tests__/method-roster-profiler.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-remote.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture-remote.integration.test.d.ts.map +1 -0
- package/dist/__tests__/server.unit.test.d.ts +2 -0
- package/dist/__tests__/server.unit.test.d.ts.map +1 -0
- package/dist/__tests__/simulation-inspect.integration.test.d.ts +2 -0
- package/dist/__tests__/simulation-inspect.integration.test.d.ts.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.mjs +336 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/defineSubcommand.d.ts +40 -0
- package/dist/defineSubcommand.d.ts.map +1 -0
- package/dist/error-messages.d.ts +3 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/errors.d.ts +82 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +37 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/execute.d.ts +32 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.mjs +95 -0
- package/dist/execute.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +30 -0
- package/dist/index.mjs.map +1 -0
- package/dist/introspect.d.ts +23 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.mjs +170 -0
- package/dist/introspect.mjs.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.mjs +435 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +81 -0
- package/src/__tests__/cli-defaults.unit.test.ts +69 -0
- package/src/__tests__/console.unit.test.ts +679 -0
- package/src/__tests__/errors.unit.test.ts +88 -0
- package/src/__tests__/execute-browser-safe.unit.test.ts +43 -0
- package/src/__tests__/execute-profiler-root.browser.test.ts +41 -0
- package/src/__tests__/execute.async.test.ts +373 -0
- package/src/__tests__/execution-report-root.unit.test.ts +38 -0
- package/src/__tests__/introspect-profiler.unit.test.ts +55 -0
- package/src/__tests__/method-roster-profiler.test.ts +80 -0
- package/src/__tests__/rhi-capture-remote.integration.test.ts +58 -0
- package/src/__tests__/server.unit.test.ts +613 -0
- package/src/__tests__/simulation-inspect.integration.test.ts +67 -0
- package/src/__tests__/vm-async-eval-verify.mjs +149 -0
- package/src/cli.ts +333 -0
- package/src/defineSubcommand.ts +169 -0
- package/src/error-messages.ts +9 -0
- package/src/errors.ts +143 -0
- package/src/execute.ts +155 -0
- package/src/index.ts +20 -0
- package/src/introspect.ts +218 -0
- package/src/server.ts +285 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// @forgeax/engine-remote/src/server — in-process remote eval server.
|
|
2
|
+
//
|
|
3
|
+
// Wire format: JSON-RPC 2.0 with two methods:
|
|
4
|
+
// - introspect() -> OpenRPC L2 subset doc
|
|
5
|
+
// - eval({script}) -> eval the script against world/renderer/assets
|
|
6
|
+
//
|
|
7
|
+
// Lifecycle:
|
|
8
|
+
// startServer({ port, host?, world, renderer?, assets? })
|
|
9
|
+
// -> Promise<Result<ConsoleHandle, RemoteError>>
|
|
10
|
+
//
|
|
11
|
+
// The sandbox layer is dismantled — eval is full-access (route B).
|
|
12
|
+
|
|
13
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
14
|
+
import type { WebSocket } from 'ws';
|
|
15
|
+
import { WebSocketServer } from 'ws';
|
|
16
|
+
import { REMOTE_ERROR_MESSAGES } from './error-messages';
|
|
17
|
+
import { REMOTE_ERROR_CODE_TO_JSONRPC, RemoteError } from './errors';
|
|
18
|
+
import { executeScript } from './execute';
|
|
19
|
+
import {
|
|
20
|
+
buildIntrospectDoc,
|
|
21
|
+
type ComponentIntrospectionDescriptor,
|
|
22
|
+
isExecutionRoot,
|
|
23
|
+
isProfilerRoot,
|
|
24
|
+
} from './introspect';
|
|
25
|
+
|
|
26
|
+
export type { ComponentIntrospectionDescriptor } from './introspect';
|
|
27
|
+
|
|
28
|
+
const REMOTE_TO_JSONRPC = REMOTE_ERROR_CODE_TO_JSONRPC;
|
|
29
|
+
|
|
30
|
+
export type { Result };
|
|
31
|
+
|
|
32
|
+
export type ConsoleHandle = {
|
|
33
|
+
readonly port: number;
|
|
34
|
+
readonly close: () => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type StartServerOptions = {
|
|
38
|
+
readonly port: number;
|
|
39
|
+
readonly host?: string;
|
|
40
|
+
readonly world: unknown;
|
|
41
|
+
readonly renderer?: unknown;
|
|
42
|
+
readonly assets?: unknown;
|
|
43
|
+
/** JSON-safe host reflection; the remote package does not know component owners. */
|
|
44
|
+
readonly introspection?: readonly ComponentIntrospectionDescriptor[];
|
|
45
|
+
/** Explicit CPU profiler capability; omitted unless the host opts in. */
|
|
46
|
+
readonly profiler?: unknown;
|
|
47
|
+
/** Structural report provider; remote never imports the App owner. */
|
|
48
|
+
readonly execution?: unknown;
|
|
49
|
+
/** Read-only World-owned simulation inspection root. */
|
|
50
|
+
readonly simulation?: unknown;
|
|
51
|
+
/**
|
|
52
|
+
* Host-owned RHI capture capability for eval-scope injection (plan-strategy D-4).
|
|
53
|
+
* It is exposed as the single `rhiCapture` eval root.
|
|
54
|
+
* Undefined when FORGEAX_ENGINE_RHI_DEBUG !== '1'.
|
|
55
|
+
*/
|
|
56
|
+
readonly rhiCapture?: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type JsonRpcRequest = {
|
|
60
|
+
jsonrpc?: unknown;
|
|
61
|
+
method?: unknown;
|
|
62
|
+
params?: unknown;
|
|
63
|
+
id?: unknown;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type JsonRpcError = {
|
|
67
|
+
code: number;
|
|
68
|
+
message: string;
|
|
69
|
+
data?: unknown;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type JsonRpcResponse = {
|
|
73
|
+
jsonrpc: '2.0';
|
|
74
|
+
id: number | string | null;
|
|
75
|
+
} & ({ result: unknown } | { error: JsonRpcError });
|
|
76
|
+
|
|
77
|
+
function inspectorErrorToJsonRpc(e: RemoteError): JsonRpcError {
|
|
78
|
+
const detail = (e as unknown as { detail?: unknown }).detail;
|
|
79
|
+
const data: Record<string, unknown> = {
|
|
80
|
+
code: e.code,
|
|
81
|
+
expected: e.expected,
|
|
82
|
+
hint: e.hint,
|
|
83
|
+
message: e.message,
|
|
84
|
+
};
|
|
85
|
+
if (detail !== undefined) data.detail = detail;
|
|
86
|
+
return {
|
|
87
|
+
code: REMOTE_TO_JSONRPC[e.code],
|
|
88
|
+
message: REMOTE_ERROR_MESSAGES[e.code],
|
|
89
|
+
data,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function respondError(
|
|
94
|
+
id: number | string | null,
|
|
95
|
+
code: number,
|
|
96
|
+
message: string,
|
|
97
|
+
data?: unknown,
|
|
98
|
+
): JsonRpcResponse {
|
|
99
|
+
const error: JsonRpcError = { code, message };
|
|
100
|
+
if (data !== undefined) {
|
|
101
|
+
error.data = data;
|
|
102
|
+
}
|
|
103
|
+
return { jsonrpc: '2.0', id, error };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function respondOk(id: number | string | null, result: unknown): JsonRpcResponse {
|
|
107
|
+
return { jsonrpc: '2.0', id, result };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isValidId(v: unknown): v is number | string | null {
|
|
111
|
+
return typeof v === 'number' || typeof v === 'string' || v === null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleEnvelope(
|
|
115
|
+
raw: string,
|
|
116
|
+
ctx: {
|
|
117
|
+
world: unknown;
|
|
118
|
+
renderer: unknown;
|
|
119
|
+
assets: unknown;
|
|
120
|
+
rhiCapture: unknown | undefined;
|
|
121
|
+
introspection: readonly ComponentIntrospectionDescriptor[];
|
|
122
|
+
profiler: unknown | undefined;
|
|
123
|
+
execution: unknown | undefined;
|
|
124
|
+
simulation: unknown | undefined;
|
|
125
|
+
host: string;
|
|
126
|
+
port: number;
|
|
127
|
+
},
|
|
128
|
+
): Promise<JsonRpcResponse | null> {
|
|
129
|
+
let parsed: JsonRpcRequest;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(raw) as JsonRpcRequest;
|
|
132
|
+
} catch {
|
|
133
|
+
return respondError(null, -32700, 'Parse error');
|
|
134
|
+
}
|
|
135
|
+
if (typeof parsed.method !== 'string') {
|
|
136
|
+
const id = isValidId(parsed.id) ? parsed.id : null;
|
|
137
|
+
return respondError(id, -32600, 'Invalid Request');
|
|
138
|
+
}
|
|
139
|
+
const isNotification = !('id' in parsed);
|
|
140
|
+
const id = isValidId(parsed.id) ? parsed.id : null;
|
|
141
|
+
|
|
142
|
+
let response: JsonRpcResponse;
|
|
143
|
+
if (parsed.method === 'introspect') {
|
|
144
|
+
response = respondOk(
|
|
145
|
+
id,
|
|
146
|
+
buildIntrospectDoc(ctx.host, ctx.port, {
|
|
147
|
+
world: ctx.world,
|
|
148
|
+
renderer: ctx.renderer,
|
|
149
|
+
assets: ctx.assets,
|
|
150
|
+
...(ctx.rhiCapture !== undefined ? { rhiCapture: ctx.rhiCapture } : {}),
|
|
151
|
+
...(ctx.profiler !== undefined ? { profiler: ctx.profiler } : {}),
|
|
152
|
+
...(ctx.execution !== undefined ? { execution: ctx.execution } : {}),
|
|
153
|
+
...(ctx.simulation !== undefined ? { simulation: ctx.simulation } : {}),
|
|
154
|
+
...(ctx.introspection.length > 0 ? { introspection: ctx.introspection } : {}),
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
} else if (parsed.method === 'eval') {
|
|
158
|
+
const params = parsed.params as { script?: unknown } | undefined;
|
|
159
|
+
const script = params?.script;
|
|
160
|
+
if (typeof script !== 'string') {
|
|
161
|
+
response = respondError(id, -32602, 'Invalid params: eval requires { script: string }');
|
|
162
|
+
} else {
|
|
163
|
+
const result = await executeScript(script, {
|
|
164
|
+
world: ctx.world,
|
|
165
|
+
renderer: ctx.renderer,
|
|
166
|
+
assets: ctx.assets,
|
|
167
|
+
rhiCapture: ctx.rhiCapture,
|
|
168
|
+
profiler: ctx.profiler,
|
|
169
|
+
execution: ctx.execution,
|
|
170
|
+
simulation: ctx.simulation,
|
|
171
|
+
});
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
response = respondOk(id, result.value);
|
|
174
|
+
} else {
|
|
175
|
+
response = { jsonrpc: '2.0', id, error: inspectorErrorToJsonRpc(result.error) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
response = respondError(id, -32601, `Method not found: ${parsed.method}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return isNotification ? null : response;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Start the remote eval WebSocket server.
|
|
187
|
+
*/
|
|
188
|
+
export function startServer(opts: StartServerOptions): Promise<Result<ConsoleHandle, RemoteError>> {
|
|
189
|
+
return new Promise<Result<ConsoleHandle, RemoteError>>((resolve) => {
|
|
190
|
+
const host = opts.host ?? '127.0.0.1';
|
|
191
|
+
if (host !== '127.0.0.1' && host !== 'localhost') {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[@forgeax/engine-remote] WARNING: binding on non-loopback host '${host}'. P0 trusts localhost only.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const wss = new WebSocketServer({
|
|
197
|
+
host,
|
|
198
|
+
port: opts.port,
|
|
199
|
+
path: '/inspector',
|
|
200
|
+
maxPayload: 1 << 20,
|
|
201
|
+
perMessageDeflate: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const world = opts.world;
|
|
205
|
+
const renderer = opts.renderer ?? {};
|
|
206
|
+
const assets = opts.assets ?? {};
|
|
207
|
+
const rhiCapture = opts.rhiCapture;
|
|
208
|
+
const introspection = opts.introspection ?? [];
|
|
209
|
+
const profiler = isProfilerRoot(opts.profiler) ? opts.profiler : undefined;
|
|
210
|
+
const execution = isExecutionRoot(opts.execution) ? opts.execution : undefined;
|
|
211
|
+
const simulation = opts.simulation;
|
|
212
|
+
let settled = false;
|
|
213
|
+
let boundPort = opts.port;
|
|
214
|
+
|
|
215
|
+
wss.on('error', (e: NodeJS.ErrnoException) => {
|
|
216
|
+
if (settled) {
|
|
217
|
+
console.error('[@forgeax/engine-remote] post-startup error:', e);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
settled = true;
|
|
221
|
+
const errno = e.code ?? 'unknown';
|
|
222
|
+
resolve(
|
|
223
|
+
err(
|
|
224
|
+
new RemoteError({
|
|
225
|
+
code: 'server-startup-failed',
|
|
226
|
+
expected: 'server starts successfully on requested port',
|
|
227
|
+
hint:
|
|
228
|
+
errno === 'EADDRINUSE'
|
|
229
|
+
? `port ${opts.port} is already in use; lsof -i :${opts.port} or pick a different port`
|
|
230
|
+
: `listen failed with errno ${errno}; check host '${host}' on this machine; port=${opts.port}`,
|
|
231
|
+
}),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
237
|
+
ws.on('message', (raw) => {
|
|
238
|
+
const text = typeof raw === 'string' ? raw : raw.toString();
|
|
239
|
+
handleEnvelope(text, {
|
|
240
|
+
world,
|
|
241
|
+
renderer,
|
|
242
|
+
assets,
|
|
243
|
+
rhiCapture,
|
|
244
|
+
introspection,
|
|
245
|
+
profiler,
|
|
246
|
+
execution,
|
|
247
|
+
simulation,
|
|
248
|
+
host,
|
|
249
|
+
port: boundPort,
|
|
250
|
+
})
|
|
251
|
+
.then((response) => {
|
|
252
|
+
if (response !== null && ws.readyState === ws.OPEN) {
|
|
253
|
+
ws.send(JSON.stringify(response));
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.catch((e: unknown) => {
|
|
257
|
+
if (ws.readyState === ws.OPEN) {
|
|
258
|
+
ws.send(JSON.stringify(respondError(null, -32603, `Internal error: ${String(e)}`)));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
wss.on('listening', () => {
|
|
265
|
+
if (settled) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
settled = true;
|
|
269
|
+
const address = wss.address();
|
|
270
|
+
const port = typeof address === 'object' && address !== null ? address.port : opts.port;
|
|
271
|
+
boundPort = port;
|
|
272
|
+
const handle: ConsoleHandle = {
|
|
273
|
+
port,
|
|
274
|
+
close: () =>
|
|
275
|
+
new Promise<void>((closeResolve) => {
|
|
276
|
+
for (const client of wss.clients) {
|
|
277
|
+
client.terminate();
|
|
278
|
+
}
|
|
279
|
+
wss.close(() => closeResolve());
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
resolve(ok(handle));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|