@rynx-ai/daemon 0.1.0 → 0.1.9
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/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/cli.js +467 -30
- package/dist/control-client.d.ts +113 -0
- package/dist/control-client.js +792 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +12 -2
- package/dist/daemon-server.js +559 -28
- package/dist/db.js +140 -10
- package/dist/desktop-browser-host-client.d.ts +34 -0
- package/dist/desktop-browser-host-client.js +413 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +127 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2156 -0
- package/dist/index-daemon.js +28 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +36 -15
- package/dist/plugin-installer.js +420 -155
- package/dist/plugin-package.d.ts +38 -0
- package/dist/plugin-package.js +553 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +55 -13
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +61 -0
- package/dist/remote-runtime-access-store.js +386 -0
- package/dist/remote-runtime-admin.d.ts +32 -0
- package/dist/remote-runtime-admin.js +107 -0
- package/dist/remote-runtime-connection-manager.d.ts +92 -0
- package/dist/remote-runtime-connection-manager.js +1409 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +47 -0
- package/dist/remote-runtime-target-control.js +433 -0
- package/dist/remote-runtime-target-store.d.ts +53 -0
- package/dist/remote-runtime-target-store.js +307 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +57 -7
- package/dist/setup.js +9 -4
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +18 -10
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { isDaemonCoreCompatible, parseDaemonStatus, } from "@rynx-ai/protocol/remote-runtime";
|
|
4
|
+
import { parseRemoteRuntimeRpcRequest, parseRemoteRuntimeRpcResponseForMethod, REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
5
|
+
import { parsePairingOffer, } from "@rynx-ai/protocol/direct-runtime";
|
|
6
|
+
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_ENV, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_CONTEXT_FILE_ENV, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_SESSION_ID_ENV, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, parseRuntimeBrowserEndpointDescriptor, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
7
|
+
import { parseRuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
8
|
+
import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
|
|
9
|
+
export { connectResidentDesktopBrowserHost, } from "./desktop-browser-host-client.js";
|
|
10
|
+
const IDENTITY_TIMEOUT_MS = 5_000;
|
|
11
|
+
const RUNTIME_STATUS_TIMEOUT_MS = 5_000;
|
|
12
|
+
const PLUGIN_CLI_TIMEOUT_MS = 120_000;
|
|
13
|
+
const PLUGIN_CLI_BODY_MAX_BYTES = 512 * 1024;
|
|
14
|
+
const PLUGIN_CLI_STDIN_MAX_BYTES = 256 * 1024;
|
|
15
|
+
const PLUGIN_CLI_ARG_MAX_BYTES = 4 * 1024;
|
|
16
|
+
const PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES = 32 * 1024;
|
|
17
|
+
const PLUGIN_CLI_ARGS_MAX_COUNT = 64;
|
|
18
|
+
const IDENTITY_RESPONSE_MAX_BYTES = 16 * 1024;
|
|
19
|
+
const RUNTIME_STATUS_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
20
|
+
const REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
21
|
+
const RUNTIME_TARGET_RESPONSE_MAX_BYTES = 128 * 1024;
|
|
22
|
+
const RUNTIME_TARGET_REQUEST_MAX_BYTES = 64 * 1024;
|
|
23
|
+
const RUNTIME_TARGET_TIMEOUT_MS = 30_000;
|
|
24
|
+
const RESIDENT_RUNTIME_CALL_TIMEOUT_MS = 135_000;
|
|
25
|
+
const RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES = REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES;
|
|
26
|
+
const RUNTIME_BROWSER_BOOTSTRAP_TIMEOUT_MS = 5_000;
|
|
27
|
+
const RUNTIME_BROWSER_BOOTSTRAP_RESPONSE_MAX_BYTES = 16 * 1_024;
|
|
28
|
+
const RUNTIME_BROWSER_CONTEXT_MAX_BYTES = 4 * 1_024;
|
|
29
|
+
const RUNTIME_BROWSER_CONTEXT_PATH_MAX_CHARS = 4_096;
|
|
30
|
+
const PLUGIN_CLI_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
const INSTALLATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
32
|
+
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
|
|
33
|
+
/** Stable error returned by the resident daemon's typed Runtime gateway. */
|
|
34
|
+
export class ResidentRuntimeCallError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
status;
|
|
37
|
+
outcome;
|
|
38
|
+
constructor(code, message, status, options) {
|
|
39
|
+
super(message, options);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.name = "ResidentRuntimeCallError";
|
|
43
|
+
this.outcome = options?.outcome;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Connect to the one resident daemon, starting its supervisor when absent. */
|
|
47
|
+
export async function ensureResidentDaemon() {
|
|
48
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
49
|
+
return { origin: endpoint.origin };
|
|
50
|
+
}
|
|
51
|
+
/** Read the stable public identity of the single resident daemon. */
|
|
52
|
+
export async function getResidentDaemonIdentity() {
|
|
53
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
54
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
55
|
+
const response = await fetch(`${endpoint.origin}/api/internal/daemon/identity`, {
|
|
56
|
+
headers: managementHeaders(endpoint.managementToken),
|
|
57
|
+
signal: AbortSignal.timeout(IDENTITY_TIMEOUT_MS),
|
|
58
|
+
});
|
|
59
|
+
const body = await readBoundedResponse(response, IDENTITY_RESPONSE_MAX_BYTES);
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw daemonRequestError("daemon identity request", response.status, body);
|
|
62
|
+
const parsed = parseJsonObject(body, "daemon returned an invalid identity response");
|
|
63
|
+
if (!INSTALLATION_ID_PATTERN.test(readString(parsed.installationId)) ||
|
|
64
|
+
parsed.algorithm !== "Ed25519" ||
|
|
65
|
+
!/^[A-Za-z0-9_-]+$/.test(readString(parsed.publicKey)) ||
|
|
66
|
+
readString(parsed.publicKey).length > 512) {
|
|
67
|
+
throw new Error("daemon returned an invalid identity response");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
installationId: parsed.installationId,
|
|
71
|
+
algorithm: "Ed25519",
|
|
72
|
+
publicKey: parsed.publicKey,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Read the transport-independent status of the resident daemon process. */
|
|
76
|
+
export async function getResidentDaemonRuntimeStatus() {
|
|
77
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
78
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
79
|
+
const response = await fetch(`${endpoint.origin}/api/internal/daemon/runtime-status`, {
|
|
80
|
+
headers: managementHeaders(endpoint.managementToken),
|
|
81
|
+
signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
|
|
82
|
+
});
|
|
83
|
+
const body = await readBoundedResponse(response, RUNTIME_STATUS_RESPONSE_MAX_BYTES);
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw daemonRequestError("daemon Runtime status request", response.status, body);
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(body);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("daemon returned an invalid Remote Runtime status response");
|
|
92
|
+
}
|
|
93
|
+
const status = parseDaemonStatus(parsed);
|
|
94
|
+
if (!isDaemonCoreCompatible(status)) {
|
|
95
|
+
throw new Error(`resident daemon Remote Runtime Core protocol range ` +
|
|
96
|
+
`[${status.minimumCompatibleCoreVersion}, ${status.coreProtocolVersion}] is incompatible`);
|
|
97
|
+
}
|
|
98
|
+
return status;
|
|
99
|
+
}
|
|
100
|
+
/** Create one pairing offer on the resident daemon for a selected reachable route. */
|
|
101
|
+
export async function createResidentRemoteRuntimePairingOffer(input = {}) {
|
|
102
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
103
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
104
|
+
const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/pairing-offers`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
...managementHeaders(endpoint.managementToken),
|
|
108
|
+
"content-type": "application/json",
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(input),
|
|
111
|
+
signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
|
|
112
|
+
});
|
|
113
|
+
const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
|
|
114
|
+
if (!response.ok)
|
|
115
|
+
throw daemonRequestError("pairing offer request", response.status, body);
|
|
116
|
+
try {
|
|
117
|
+
return parsePairingOffer(JSON.parse(body));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new Error(`daemon returned an invalid Remote Runtime pairing offer: ${messageOf(error)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** List redacted client grants accepted by the resident daemon. */
|
|
124
|
+
export async function listResidentRemoteRuntimeClients() {
|
|
125
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
126
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
127
|
+
const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/clients`, {
|
|
128
|
+
headers: managementHeaders(endpoint.managementToken),
|
|
129
|
+
signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
|
|
130
|
+
});
|
|
131
|
+
const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
|
|
132
|
+
if (!response.ok)
|
|
133
|
+
throw daemonRequestError("Remote Runtime clients request", response.status, body);
|
|
134
|
+
const parsed = parseJsonObject(body, "daemon returned an invalid Remote Runtime clients response");
|
|
135
|
+
if (!Array.isArray(parsed.clients)) {
|
|
136
|
+
throw new Error("daemon returned an invalid Remote Runtime clients response");
|
|
137
|
+
}
|
|
138
|
+
return parsed.clients.map(parseRemoteRuntimeClientGrant);
|
|
139
|
+
}
|
|
140
|
+
/** Revoke one resident-daemon client grant and close its active channels. */
|
|
141
|
+
export async function revokeResidentRemoteRuntimeClient(grantId) {
|
|
142
|
+
if (!OPAQUE_ID_PATTERN.test(grantId) || grantId.length > 256) {
|
|
143
|
+
throw new Error("invalid Remote Runtime grant id");
|
|
144
|
+
}
|
|
145
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
146
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
147
|
+
const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/clients/${encodeURIComponent(grantId)}`, {
|
|
148
|
+
method: "DELETE",
|
|
149
|
+
headers: managementHeaders(endpoint.managementToken),
|
|
150
|
+
signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
|
|
151
|
+
});
|
|
152
|
+
const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
|
|
153
|
+
if (!response.ok)
|
|
154
|
+
throw daemonRequestError("Remote Runtime client revoke", response.status, body);
|
|
155
|
+
const parsed = parseJsonObject(body, "daemon returned an invalid client revoke response");
|
|
156
|
+
if (parsed.revoked !== true ||
|
|
157
|
+
!Number.isSafeInteger(parsed.closedConnections) ||
|
|
158
|
+
parsed.closedConnections < 0) {
|
|
159
|
+
throw new Error("daemon returned an invalid client revoke response");
|
|
160
|
+
}
|
|
161
|
+
return { revoked: true, closedConnections: parsed.closedConnections };
|
|
162
|
+
}
|
|
163
|
+
/** List local and paired Runtime targets through the resident daemon BFF. */
|
|
164
|
+
export async function listResidentRuntimeTargets() {
|
|
165
|
+
const response = await runtimeTargetRequest("/api/internal/runtimes", { method: "GET" });
|
|
166
|
+
const parsed = parseJsonObject(response, "daemon returned an invalid Runtime target list");
|
|
167
|
+
if (!Array.isArray(parsed.runtimes)) {
|
|
168
|
+
throw new Error("daemon returned an invalid Runtime target list");
|
|
169
|
+
}
|
|
170
|
+
return parsed.runtimes.map(parseRuntimeTargetView);
|
|
171
|
+
}
|
|
172
|
+
/** Pair a target; the offer remains inside the loopback management request. */
|
|
173
|
+
export async function pairResidentRuntimeTarget(offer, displayName) {
|
|
174
|
+
const body = JSON.stringify({ offer, ...(displayName ? { displayName } : {}) });
|
|
175
|
+
if (Buffer.byteLength(body, "utf8") > RUNTIME_TARGET_REQUEST_MAX_BYTES) {
|
|
176
|
+
throw new Error(`Runtime target pairing request exceeds ${RUNTIME_TARGET_REQUEST_MAX_BYTES} bytes`);
|
|
177
|
+
}
|
|
178
|
+
const response = await runtimeTargetRequest("/api/internal/runtimes/pair", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
body,
|
|
181
|
+
});
|
|
182
|
+
return parseRuntimeTargetResult(response, "pairing");
|
|
183
|
+
}
|
|
184
|
+
export async function testResidentRuntimeTarget(selector) {
|
|
185
|
+
const normalized = validRuntimeSelector(selector);
|
|
186
|
+
const response = await runtimeTargetRequest(`/api/internal/runtimes/${encodeURIComponent(normalized)}/test`, { method: "POST", body: "{}" });
|
|
187
|
+
return parseRuntimeTargetResult(response, "test");
|
|
188
|
+
}
|
|
189
|
+
export async function forgetResidentRuntimeTarget(daemonId) {
|
|
190
|
+
const normalized = validRuntimeSelector(daemonId);
|
|
191
|
+
const response = await runtimeTargetRequest(`/api/internal/runtimes/${encodeURIComponent(normalized)}`, { method: "DELETE" });
|
|
192
|
+
const parsed = parseJsonObject(response, "daemon returned an invalid Runtime forget response");
|
|
193
|
+
if (parsed.forgotten !== true) {
|
|
194
|
+
throw new Error("daemon returned an invalid Runtime forget response");
|
|
195
|
+
}
|
|
196
|
+
return { forgotten: true };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Invoke one typed Runtime method through the single resident daemon.
|
|
200
|
+
*
|
|
201
|
+
* The short-lived CLI process never reads Direct credentials or opens a
|
|
202
|
+
* remote socket. The resident daemon owns target resolution, Local/Direct
|
|
203
|
+
* connection reuse, authorization and no-retry outcome semantics.
|
|
204
|
+
*/
|
|
205
|
+
export async function callResidentRuntime(selectorInput, method, params, options = {}) {
|
|
206
|
+
let selector;
|
|
207
|
+
try {
|
|
208
|
+
selector = validRuntimeSelector(selectorInput);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw new ResidentRuntimeCallError("invalid_target", "Runtime target selector is invalid", undefined, { cause: error, outcome: "not_started" });
|
|
212
|
+
}
|
|
213
|
+
let request;
|
|
214
|
+
try {
|
|
215
|
+
request = parseRemoteRuntimeRpcRequest({
|
|
216
|
+
type: "rpc.request",
|
|
217
|
+
id: "resident-runtime-call",
|
|
218
|
+
method,
|
|
219
|
+
params,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
throw new ResidentRuntimeCallError("invalid_request", `Runtime ${method} parameters are invalid`, undefined, { cause: error, outcome: "not_started" });
|
|
224
|
+
}
|
|
225
|
+
const body = JSON.stringify({ method: request.method, params: request.params });
|
|
226
|
+
if (Buffer.byteLength(body, "utf8") > REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES) {
|
|
227
|
+
throw new ResidentRuntimeCallError("invalid_request", `Runtime request exceeds ${REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES} bytes`, undefined, { outcome: "not_started" });
|
|
228
|
+
}
|
|
229
|
+
if (options.signal?.aborted) {
|
|
230
|
+
throw new ResidentRuntimeCallError("cancelled", "Runtime call was cancelled before it started", undefined, { cause: options.signal.reason, outcome: "not_started" });
|
|
231
|
+
}
|
|
232
|
+
const timeoutSignal = AbortSignal.timeout(RESIDENT_RUNTIME_CALL_TIMEOUT_MS);
|
|
233
|
+
const signal = options.signal
|
|
234
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
235
|
+
: timeoutSignal;
|
|
236
|
+
let endpoint;
|
|
237
|
+
try {
|
|
238
|
+
endpoint = await ensureDaemonControlEndpoint({ signal });
|
|
239
|
+
signal.throwIfAborted();
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (options.signal?.aborted) {
|
|
243
|
+
throw new ResidentRuntimeCallError("cancelled", "Runtime call was cancelled before it started", undefined, { cause: error, outcome: "not_started" });
|
|
244
|
+
}
|
|
245
|
+
throw new ResidentRuntimeCallError(timeoutSignal.aborted ? "deadline_exceeded" : "unreachable", timeoutSignal.aborted
|
|
246
|
+
? "Resident daemon startup exceeded the Runtime call deadline"
|
|
247
|
+
: "Resident daemon is unavailable", undefined, { cause: error, outcome: "not_started" });
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
throw new ResidentRuntimeCallError("protocol_error", "Resident daemon control endpoint is not loopback-only", undefined, { cause: error, outcome: "not_started" });
|
|
254
|
+
}
|
|
255
|
+
let response;
|
|
256
|
+
try {
|
|
257
|
+
response = await fetch(`${endpoint.origin}/api/internal/runtimes/${encodeURIComponent(selector)}/rpc`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: {
|
|
260
|
+
...managementHeaders(endpoint.managementToken),
|
|
261
|
+
"content-type": "application/json",
|
|
262
|
+
},
|
|
263
|
+
body,
|
|
264
|
+
signal,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
|
|
269
|
+
if (options.signal?.aborted) {
|
|
270
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "cancelled", neverRetry
|
|
271
|
+
? `Runtime may have applied ${method} before local cancellation`
|
|
272
|
+
: `Runtime ${method} was cancelled`, undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
273
|
+
}
|
|
274
|
+
if (isRequestTimeout(error) || timeoutSignal.aborted) {
|
|
275
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "deadline_exceeded", neverRetry
|
|
276
|
+
? `Runtime may have applied ${method} before the local request timed out`
|
|
277
|
+
: `Runtime ${method} exceeded the local request deadline`, undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
278
|
+
}
|
|
279
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "unreachable", neverRetry
|
|
280
|
+
? `Runtime may have applied ${method} before the local connection failed`
|
|
281
|
+
: "Resident Runtime request could not reach the selected target", undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
282
|
+
}
|
|
283
|
+
const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
|
|
284
|
+
let responseBody;
|
|
285
|
+
try {
|
|
286
|
+
responseBody = await readBoundedResponse(response, RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
|
|
290
|
+
? `Runtime may have applied ${method}, but its response was incomplete`
|
|
291
|
+
: "daemon returned an unreadable Runtime call response", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
292
|
+
}
|
|
293
|
+
if (!response.ok)
|
|
294
|
+
throw parseResidentRuntimeCallError(response.status, responseBody);
|
|
295
|
+
try {
|
|
296
|
+
const envelope = parseJsonObject(responseBody, "daemon returned an invalid Runtime call response");
|
|
297
|
+
if (Object.keys(envelope).length !== 1 || !Object.hasOwn(envelope, "result")) {
|
|
298
|
+
throw new Error("Runtime result envelope is invalid");
|
|
299
|
+
}
|
|
300
|
+
const parsed = parseRemoteRuntimeRpcResponseForMethod({
|
|
301
|
+
type: "rpc.result",
|
|
302
|
+
id: "resident-runtime-call",
|
|
303
|
+
result: envelope.result,
|
|
304
|
+
}, method);
|
|
305
|
+
if (parsed.type !== "rpc.result")
|
|
306
|
+
throw new Error("expected a Runtime result");
|
|
307
|
+
return parsed.result;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
|
|
311
|
+
? `Runtime may have applied ${method}, but its result was invalid`
|
|
312
|
+
: "daemon returned an invalid Runtime call result", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Read the current managed Session credential without exposing it in output. */
|
|
316
|
+
export async function readManagedRuntimeBrowserCredential(env = process.env) {
|
|
317
|
+
const contextFile = env[RUNTIME_BROWSER_CONTEXT_FILE_ENV];
|
|
318
|
+
if (!contextFile) {
|
|
319
|
+
try {
|
|
320
|
+
return parseRuntimeBrowserBootstrapCredential({
|
|
321
|
+
sessionId: env[RUNTIME_BROWSER_SESSION_ID_ENV],
|
|
322
|
+
capability: env[RUNTIME_BROWSER_CAPABILITY_ENV],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
throw new Error("This command must run inside a managed Rynx Session", { cause: error });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (contextFile.length > RUNTIME_BROWSER_CONTEXT_PATH_MAX_CHARS ||
|
|
330
|
+
contextFile.includes("\0") ||
|
|
331
|
+
!isAbsolute(contextFile)) {
|
|
332
|
+
throw new Error("Managed Runtime Browser context path is invalid");
|
|
333
|
+
}
|
|
334
|
+
const file = await open(contextFile, "r");
|
|
335
|
+
try {
|
|
336
|
+
const stat = await file.stat();
|
|
337
|
+
if (!stat.isFile() || stat.size > RUNTIME_BROWSER_CONTEXT_MAX_BYTES) {
|
|
338
|
+
throw new Error("Managed Runtime Browser context file is invalid");
|
|
339
|
+
}
|
|
340
|
+
const raw = await file.readFile({ encoding: "utf8" });
|
|
341
|
+
if (Buffer.byteLength(raw, "utf8") > RUNTIME_BROWSER_CONTEXT_MAX_BYTES) {
|
|
342
|
+
throw new Error("Managed Runtime Browser context file is too large");
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
return parseRuntimeBrowserBootstrapCredential(JSON.parse(raw));
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
throw new Error("Managed Runtime Browser context is invalid", { cause: error });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
await file.close();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Return undefined only when the process has no managed Session context at all. */
|
|
356
|
+
export async function readOptionalManagedRuntimeBrowserCredential(env = process.env) {
|
|
357
|
+
if (env[RUNTIME_BROWSER_CONTEXT_FILE_ENV] === undefined &&
|
|
358
|
+
env[RUNTIME_BROWSER_SESSION_ID_ENV] === undefined &&
|
|
359
|
+
env[RUNTIME_BROWSER_CAPABILITY_ENV] === undefined)
|
|
360
|
+
return undefined;
|
|
361
|
+
return readManagedRuntimeBrowserCredential(env);
|
|
362
|
+
}
|
|
363
|
+
const MANAGED_BROWSER_RPC_METHODS = new Set([
|
|
364
|
+
"browser.state.get",
|
|
365
|
+
"browser.open",
|
|
366
|
+
"browser.close",
|
|
367
|
+
"browser.page.create",
|
|
368
|
+
"browser.page.close",
|
|
369
|
+
"browser.page.activate",
|
|
370
|
+
"browser.page.navigate",
|
|
371
|
+
"browser.page.back",
|
|
372
|
+
"browser.page.forward",
|
|
373
|
+
"browser.page.reload",
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* Invoke Browser control for the caller's own managed Session. Unlike the
|
|
377
|
+
* operator Runtime gateway, this path never reads or sends daemon management
|
|
378
|
+
* authority and cannot select a remote Runtime or another Session.
|
|
379
|
+
*/
|
|
380
|
+
export async function callManagedRuntimeBrowser(credential, method, params, options = {}) {
|
|
381
|
+
const validatedCredential = parseRuntimeBrowserBootstrapCredential(credential);
|
|
382
|
+
let request;
|
|
383
|
+
try {
|
|
384
|
+
request = parseRemoteRuntimeRpcRequest({
|
|
385
|
+
type: "rpc.request",
|
|
386
|
+
id: "managed-session-browser",
|
|
387
|
+
method,
|
|
388
|
+
params,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
throw new ResidentRuntimeCallError("invalid_request", `Runtime ${method} parameters are invalid`, undefined, { cause: error, outcome: "not_started" });
|
|
393
|
+
}
|
|
394
|
+
if (!MANAGED_BROWSER_RPC_METHODS.has(request.method) ||
|
|
395
|
+
!("sessionId" in request.params) ||
|
|
396
|
+
request.params.sessionId !== validatedCredential.sessionId) {
|
|
397
|
+
throw new ResidentRuntimeCallError("unauthorized", "Managed Browser control is limited to the caller's own Session", undefined, { outcome: "not_started" });
|
|
398
|
+
}
|
|
399
|
+
const timeoutSignal = AbortSignal.timeout(RESIDENT_RUNTIME_CALL_TIMEOUT_MS);
|
|
400
|
+
const signal = options.signal
|
|
401
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
402
|
+
: timeoutSignal;
|
|
403
|
+
const endpoint = await ensureDaemonControlEndpoint({ signal });
|
|
404
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
405
|
+
let response;
|
|
406
|
+
try {
|
|
407
|
+
response = await fetch(`${endpoint.origin}${RUNTIME_BROWSER_RPC_PATH}`, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"content-type": "application/json",
|
|
411
|
+
[RUNTIME_BROWSER_SESSION_ID_HEADER]: validatedCredential.sessionId,
|
|
412
|
+
[RUNTIME_BROWSER_CAPABILITY_HEADER]: validatedCredential.capability,
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify({ method: request.method, params: request.params }),
|
|
415
|
+
signal,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
|
|
420
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : timeoutSignal.aborted ? "deadline_exceeded" : "unreachable", neverRetry
|
|
421
|
+
? `Runtime may have applied ${method} before the local connection failed`
|
|
422
|
+
: timeoutSignal.aborted
|
|
423
|
+
? `Runtime ${method} exceeded the local request deadline`
|
|
424
|
+
: "Resident daemon is unavailable", undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
425
|
+
}
|
|
426
|
+
const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
|
|
427
|
+
let body;
|
|
428
|
+
try {
|
|
429
|
+
body = await readBoundedResponse(response, RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
|
|
433
|
+
? `Runtime may have applied ${method}, but its response was incomplete`
|
|
434
|
+
: "daemon returned an unreadable Runtime call response", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
435
|
+
}
|
|
436
|
+
if (!response.ok)
|
|
437
|
+
throw parseResidentRuntimeCallError(response.status, body);
|
|
438
|
+
try {
|
|
439
|
+
const envelope = parseJsonObject(body, "daemon returned an invalid Runtime call response");
|
|
440
|
+
if (Object.keys(envelope).length !== 1 || !Object.hasOwn(envelope, "result")) {
|
|
441
|
+
throw new Error("Runtime result envelope is invalid");
|
|
442
|
+
}
|
|
443
|
+
const parsed = parseRemoteRuntimeRpcResponseForMethod({
|
|
444
|
+
type: "rpc.result",
|
|
445
|
+
id: "managed-session-browser",
|
|
446
|
+
result: envelope.result,
|
|
447
|
+
}, method);
|
|
448
|
+
if (parsed.type !== "rpc.result")
|
|
449
|
+
throw new Error("expected a Runtime result");
|
|
450
|
+
return parsed.result;
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
|
|
454
|
+
? `Runtime may have applied ${method}, but its result was invalid`
|
|
455
|
+
: "daemon returned an invalid Runtime call result", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Resolve native CDP for the caller's own Session on this Runtime only. */
|
|
459
|
+
export async function getResidentRuntimeLocalBrowserEndpoint(options = {}) {
|
|
460
|
+
const credential = options.sessionId === undefined
|
|
461
|
+
? await readManagedRuntimeBrowserCredential(options.env)
|
|
462
|
+
: undefined;
|
|
463
|
+
const explicitSessionId = options.sessionId === undefined
|
|
464
|
+
? undefined
|
|
465
|
+
: parseRuntimeBrowserStateGetParams({ sessionId: options.sessionId }).sessionId;
|
|
466
|
+
const timeoutSignal = AbortSignal.timeout(RUNTIME_BROWSER_BOOTSTRAP_TIMEOUT_MS);
|
|
467
|
+
const signal = options.signal
|
|
468
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
469
|
+
: timeoutSignal;
|
|
470
|
+
const endpoint = await ensureDaemonControlEndpoint({ signal });
|
|
471
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
472
|
+
const path = credential
|
|
473
|
+
? RUNTIME_BROWSER_BOOTSTRAP_PATH
|
|
474
|
+
: `${RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX}/${encodeURIComponent(explicitSessionId)}/endpoint`;
|
|
475
|
+
const response = await fetch(`${endpoint.origin}${path}`, {
|
|
476
|
+
headers: credential
|
|
477
|
+
? {
|
|
478
|
+
[RUNTIME_BROWSER_SESSION_ID_HEADER]: credential.sessionId,
|
|
479
|
+
[RUNTIME_BROWSER_CAPABILITY_HEADER]: credential.capability,
|
|
480
|
+
}
|
|
481
|
+
: managementHeaders(endpoint.managementToken),
|
|
482
|
+
signal,
|
|
483
|
+
});
|
|
484
|
+
const body = await readBoundedResponse(response, RUNTIME_BROWSER_BOOTSTRAP_RESPONSE_MAX_BYTES);
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
throw daemonRequestError("Runtime Browser endpoint request", response.status, body);
|
|
487
|
+
}
|
|
488
|
+
let descriptor;
|
|
489
|
+
try {
|
|
490
|
+
descriptor = parseRuntimeBrowserEndpointDescriptor(JSON.parse(body));
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
throw new Error("daemon returned an invalid Runtime Browser endpoint", { cause: error });
|
|
494
|
+
}
|
|
495
|
+
if (descriptor.sessionId !== (credential?.sessionId ?? explicitSessionId)) {
|
|
496
|
+
throw new Error("daemon returned a Runtime Browser endpoint for another Session");
|
|
497
|
+
}
|
|
498
|
+
return descriptor;
|
|
499
|
+
}
|
|
500
|
+
/** Invoke one command declared by an installed plugin through the resident daemon. */
|
|
501
|
+
export async function invokeResidentPluginCommand(pluginId, args, options = {}) {
|
|
502
|
+
validatePluginInvocation(pluginId, args, options.stdin);
|
|
503
|
+
const requestSignal = options.signal
|
|
504
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(PLUGIN_CLI_TIMEOUT_MS)])
|
|
505
|
+
: AbortSignal.timeout(PLUGIN_CLI_TIMEOUT_MS);
|
|
506
|
+
requestSignal.throwIfAborted();
|
|
507
|
+
const endpoint = await ensureDaemonControlEndpoint({ signal: requestSignal });
|
|
508
|
+
requestSignal.throwIfAborted();
|
|
509
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
510
|
+
const requestBody = JSON.stringify({
|
|
511
|
+
args,
|
|
512
|
+
...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
|
|
513
|
+
});
|
|
514
|
+
if (Buffer.byteLength(requestBody, "utf8") > PLUGIN_CLI_BODY_MAX_BYTES) {
|
|
515
|
+
throw new Error(`plugin command request exceeds ${PLUGIN_CLI_BODY_MAX_BYTES} bytes`);
|
|
516
|
+
}
|
|
517
|
+
const response = await fetch(`${endpoint.origin}/api/internal/plugins/${encodeURIComponent(pluginId)}/cli`, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: {
|
|
520
|
+
...managementHeaders(endpoint.managementToken),
|
|
521
|
+
"content-type": "application/json",
|
|
522
|
+
},
|
|
523
|
+
body: requestBody,
|
|
524
|
+
signal: requestSignal,
|
|
525
|
+
});
|
|
526
|
+
const body = await readBoundedResponse(response, PLUGIN_CLI_RESPONSE_MAX_BYTES);
|
|
527
|
+
if (!response.ok)
|
|
528
|
+
throw daemonRequestError("plugin command request", response.status, body);
|
|
529
|
+
const parsed = parseJsonObject(body, "daemon returned an invalid plugin command response");
|
|
530
|
+
if (!Number.isInteger(parsed.code) ||
|
|
531
|
+
parsed.code < 0 ||
|
|
532
|
+
parsed.code > 255 ||
|
|
533
|
+
typeof parsed.stdout !== "string" ||
|
|
534
|
+
typeof parsed.stderr !== "string" ||
|
|
535
|
+
typeof parsed.truncated !== "boolean") {
|
|
536
|
+
throw new Error("daemon returned an invalid plugin command response");
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
code: parsed.code,
|
|
540
|
+
stdout: parsed.stdout,
|
|
541
|
+
stderr: parsed.stderr,
|
|
542
|
+
truncated: parsed.truncated,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function managementHeaders(token) {
|
|
546
|
+
return { "x-rynx-management-token": token };
|
|
547
|
+
}
|
|
548
|
+
function assertLoopbackOrigin(origin) {
|
|
549
|
+
const hostname = new URL(origin).hostname;
|
|
550
|
+
if (hostname !== "127.0.0.1" && hostname !== "[::1]" && hostname !== "::1") {
|
|
551
|
+
throw new Error("resident daemon control endpoint is not loopback-only");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function validatePluginInvocation(pluginId, args, stdin) {
|
|
555
|
+
if (!PLUGIN_ID_PATTERN.test(pluginId))
|
|
556
|
+
throw new Error("invalid plugin id");
|
|
557
|
+
if (args.length === 0)
|
|
558
|
+
throw new Error("plugin command args must not be empty");
|
|
559
|
+
if (args.length > PLUGIN_CLI_ARGS_MAX_COUNT) {
|
|
560
|
+
throw new Error(`plugin command args exceed ${PLUGIN_CLI_ARGS_MAX_COUNT} entries`);
|
|
561
|
+
}
|
|
562
|
+
let totalBytes = 0;
|
|
563
|
+
for (const arg of args) {
|
|
564
|
+
if (typeof arg !== "string" || arg.includes("\0")) {
|
|
565
|
+
throw new Error("plugin command args must be strings without NUL bytes");
|
|
566
|
+
}
|
|
567
|
+
const bytes = Buffer.byteLength(arg, "utf8");
|
|
568
|
+
if (bytes > PLUGIN_CLI_ARG_MAX_BYTES) {
|
|
569
|
+
throw new Error(`plugin command arg exceeds ${PLUGIN_CLI_ARG_MAX_BYTES} bytes`);
|
|
570
|
+
}
|
|
571
|
+
totalBytes += bytes;
|
|
572
|
+
}
|
|
573
|
+
if (totalBytes > PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES) {
|
|
574
|
+
throw new Error(`plugin command args exceed ${PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES} bytes`);
|
|
575
|
+
}
|
|
576
|
+
if (stdin !== undefined && typeof stdin !== "string")
|
|
577
|
+
throw new Error("plugin command stdin must be a string");
|
|
578
|
+
if (stdin !== undefined && Buffer.byteLength(stdin, "utf8") > PLUGIN_CLI_STDIN_MAX_BYTES) {
|
|
579
|
+
throw new Error(`plugin command stdin exceeds ${PLUGIN_CLI_STDIN_MAX_BYTES} bytes`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function readBoundedResponse(response, maxBytes) {
|
|
583
|
+
if (!response.body)
|
|
584
|
+
return "";
|
|
585
|
+
const reader = response.body.getReader();
|
|
586
|
+
const chunks = [];
|
|
587
|
+
let bytes = 0;
|
|
588
|
+
try {
|
|
589
|
+
while (true) {
|
|
590
|
+
const { done, value } = await reader.read();
|
|
591
|
+
if (done)
|
|
592
|
+
break;
|
|
593
|
+
bytes += value.byteLength;
|
|
594
|
+
if (bytes > maxBytes) {
|
|
595
|
+
await reader.cancel();
|
|
596
|
+
throw new Error(`daemon response exceeds ${maxBytes} bytes`);
|
|
597
|
+
}
|
|
598
|
+
chunks.push(Buffer.from(value));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
reader.releaseLock();
|
|
603
|
+
}
|
|
604
|
+
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
605
|
+
}
|
|
606
|
+
function parseJsonObject(body, message) {
|
|
607
|
+
try {
|
|
608
|
+
const parsed = JSON.parse(body);
|
|
609
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
610
|
+
return parsed;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// Fall through to the stable protocol error below.
|
|
615
|
+
}
|
|
616
|
+
throw new Error(message);
|
|
617
|
+
}
|
|
618
|
+
function daemonRequestError(operation, status, body) {
|
|
619
|
+
let detail = body.trim().slice(0, 2_000);
|
|
620
|
+
try {
|
|
621
|
+
const parsed = JSON.parse(body);
|
|
622
|
+
if (typeof parsed.error === "string")
|
|
623
|
+
detail = parsed.error.slice(0, 2_000);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
// A bounded plain-text error is still useful for local diagnostics.
|
|
627
|
+
}
|
|
628
|
+
return new Error(`${operation} failed (${status})${detail ? `: ${detail}` : ""}`);
|
|
629
|
+
}
|
|
630
|
+
const RESIDENT_RUNTIME_CALL_ERROR_CODES = new Set([
|
|
631
|
+
"invalid_target",
|
|
632
|
+
"invalid_request",
|
|
633
|
+
"not_found",
|
|
634
|
+
"conflict",
|
|
635
|
+
"failed_precondition",
|
|
636
|
+
"method_not_found",
|
|
637
|
+
"deadline_exceeded",
|
|
638
|
+
"unreachable",
|
|
639
|
+
"tls_error",
|
|
640
|
+
"identity_mismatch",
|
|
641
|
+
"incompatible",
|
|
642
|
+
"authentication_failed",
|
|
643
|
+
"protocol_error",
|
|
644
|
+
"operation_failed",
|
|
645
|
+
"outcome_unknown",
|
|
646
|
+
"cancelled",
|
|
647
|
+
"unauthorized",
|
|
648
|
+
"closed",
|
|
649
|
+
"internal",
|
|
650
|
+
"internal_error",
|
|
651
|
+
]);
|
|
652
|
+
function parseResidentRuntimeCallError(status, body) {
|
|
653
|
+
let code = "internal_error";
|
|
654
|
+
let outcome;
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(body);
|
|
657
|
+
if (typeof parsed.error === "string" &&
|
|
658
|
+
RESIDENT_RUNTIME_CALL_ERROR_CODES.has(parsed.error)) {
|
|
659
|
+
code = parsed.error;
|
|
660
|
+
}
|
|
661
|
+
if (parsed.outcome === "not_started" || parsed.outcome === "unknown") {
|
|
662
|
+
outcome = parsed.outcome;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
// Keep the stable internal_error fallback; never expose a daemon body.
|
|
667
|
+
}
|
|
668
|
+
return new ResidentRuntimeCallError(code, `Runtime call failed (${status}): ${code}`, status, { ...(outcome ? { outcome } : code === "outcome_unknown" ? { outcome: "unknown" } : {}) });
|
|
669
|
+
}
|
|
670
|
+
function isRequestTimeout(error) {
|
|
671
|
+
return error instanceof Error &&
|
|
672
|
+
(error.name === "TimeoutError" || error.name === "AbortError");
|
|
673
|
+
}
|
|
674
|
+
function readString(value) {
|
|
675
|
+
return typeof value === "string" ? value : "";
|
|
676
|
+
}
|
|
677
|
+
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/;
|
|
678
|
+
function parseRemoteRuntimeClientGrant(value) {
|
|
679
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
680
|
+
throw new Error("daemon returned an invalid Remote Runtime client grant");
|
|
681
|
+
}
|
|
682
|
+
const record = value;
|
|
683
|
+
for (const field of ["grantId", "clientId"]) {
|
|
684
|
+
if (typeof record[field] !== "string" ||
|
|
685
|
+
record[field].length > 256 ||
|
|
686
|
+
!OPAQUE_ID_PATTERN.test(record[field])) {
|
|
687
|
+
throw new Error("daemon returned an invalid Remote Runtime client grant");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (record.clientLabel !== null && typeof record.clientLabel !== "string" ||
|
|
691
|
+
typeof record.clientKeyFingerprint !== "string" ||
|
|
692
|
+
!record.clientKeyFingerprint.startsWith("SHA256:") ||
|
|
693
|
+
!Array.isArray(record.authorities) ||
|
|
694
|
+
record.authorities.length !== 1 ||
|
|
695
|
+
record.authorities[0] !== "daemon.full" ||
|
|
696
|
+
typeof record.createdAt !== "string" ||
|
|
697
|
+
!Number.isFinite(Date.parse(record.createdAt)) ||
|
|
698
|
+
record.revokedAt !== null &&
|
|
699
|
+
(typeof record.revokedAt !== "string" || !Number.isFinite(Date.parse(record.revokedAt))) ||
|
|
700
|
+
record.lastUsedAt !== null &&
|
|
701
|
+
(typeof record.lastUsedAt !== "string" || !Number.isFinite(Date.parse(record.lastUsedAt)))) {
|
|
702
|
+
throw new Error("daemon returned an invalid Remote Runtime client grant");
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
grantId: record.grantId,
|
|
706
|
+
clientId: record.clientId,
|
|
707
|
+
clientLabel: record.clientLabel,
|
|
708
|
+
clientKeyFingerprint: record.clientKeyFingerprint,
|
|
709
|
+
authorities: ["daemon.full"],
|
|
710
|
+
createdAt: record.createdAt,
|
|
711
|
+
revokedAt: record.revokedAt,
|
|
712
|
+
lastUsedAt: record.lastUsedAt,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async function runtimeTargetRequest(pathname, input) {
|
|
716
|
+
const endpoint = await ensureDaemonControlEndpoint();
|
|
717
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
718
|
+
const response = await fetch(`${endpoint.origin}${pathname}`, {
|
|
719
|
+
method: input.method,
|
|
720
|
+
headers: {
|
|
721
|
+
...managementHeaders(endpoint.managementToken),
|
|
722
|
+
...(input.body === undefined ? {} : { "content-type": "application/json" }),
|
|
723
|
+
},
|
|
724
|
+
...(input.body === undefined ? {} : { body: input.body }),
|
|
725
|
+
signal: AbortSignal.timeout(RUNTIME_TARGET_TIMEOUT_MS),
|
|
726
|
+
});
|
|
727
|
+
const body = await readBoundedResponse(response, RUNTIME_TARGET_RESPONSE_MAX_BYTES);
|
|
728
|
+
if (!response.ok)
|
|
729
|
+
throw daemonRequestError("Runtime target request", response.status, body);
|
|
730
|
+
return body;
|
|
731
|
+
}
|
|
732
|
+
function parseRuntimeTargetResult(body, operation) {
|
|
733
|
+
const parsed = parseJsonObject(body, `daemon returned an invalid Runtime target ${operation} response`);
|
|
734
|
+
try {
|
|
735
|
+
return {
|
|
736
|
+
target: parseRuntimeTargetView(parsed.target),
|
|
737
|
+
status: parseDaemonStatus(parsed.status),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
throw new Error(`daemon returned an invalid Runtime target ${operation} response: ${messageOf(error)}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function parseRuntimeTargetView(value) {
|
|
745
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
746
|
+
throw new Error("invalid Runtime target view");
|
|
747
|
+
}
|
|
748
|
+
const record = value;
|
|
749
|
+
const selector = validRuntimeSelector(record.selector);
|
|
750
|
+
const daemonId = validRuntimeSelector(record.daemonId);
|
|
751
|
+
if (typeof record.displayName !== "string" ||
|
|
752
|
+
record.displayName.length === 0 ||
|
|
753
|
+
record.displayName.length > 128 ||
|
|
754
|
+
(record.binding !== "local" && record.binding !== "direct") ||
|
|
755
|
+
(record.endpoint !== undefined && typeof record.endpoint !== "string") ||
|
|
756
|
+
(record.identityKeyFingerprint !== undefined &&
|
|
757
|
+
(typeof record.identityKeyFingerprint !== "string" ||
|
|
758
|
+
!record.identityKeyFingerprint.startsWith("SHA256:"))) ||
|
|
759
|
+
(record.createdAt !== undefined &&
|
|
760
|
+
(typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))))) {
|
|
761
|
+
throw new Error("invalid Runtime target view");
|
|
762
|
+
}
|
|
763
|
+
if (record.binding === "local" && selector !== "local") {
|
|
764
|
+
throw new Error("invalid local Runtime target selector");
|
|
765
|
+
}
|
|
766
|
+
if (record.binding === "direct" && (selector !== daemonId || typeof record.endpoint !== "string")) {
|
|
767
|
+
throw new Error("invalid Direct Runtime target view");
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
selector,
|
|
771
|
+
daemonId,
|
|
772
|
+
displayName: record.displayName,
|
|
773
|
+
binding: record.binding,
|
|
774
|
+
...(record.endpoint === undefined ? {} : { endpoint: record.endpoint }),
|
|
775
|
+
...(record.identityKeyFingerprint === undefined
|
|
776
|
+
? {}
|
|
777
|
+
: { identityKeyFingerprint: record.identityKeyFingerprint }),
|
|
778
|
+
...(record.createdAt === undefined ? {} : { createdAt: record.createdAt }),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function validRuntimeSelector(value) {
|
|
782
|
+
if (typeof value !== "string" ||
|
|
783
|
+
value.length === 0 ||
|
|
784
|
+
value.length > 256 ||
|
|
785
|
+
!OPAQUE_ID_PATTERN.test(value)) {
|
|
786
|
+
throw new Error("invalid Runtime target selector");
|
|
787
|
+
}
|
|
788
|
+
return value;
|
|
789
|
+
}
|
|
790
|
+
function messageOf(error) {
|
|
791
|
+
return error instanceof Error ? error.message : String(error);
|
|
792
|
+
}
|