@rynx-ai/cli 0.1.10

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.
Files changed (47) hide show
  1. package/dist/agent-file.d.ts +13 -0
  2. package/dist/agent-file.js +61 -0
  3. package/dist/browser-cli-args.d.ts +28 -0
  4. package/dist/browser-cli-args.js +181 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +11 -0
  7. package/dist/client.d.ts +4 -0
  8. package/dist/client.js +2 -0
  9. package/dist/commands/agent.d.ts +1 -0
  10. package/dist/commands/agent.js +55 -0
  11. package/dist/commands/app-distribution.d.ts +6 -0
  12. package/dist/commands/app-distribution.js +98 -0
  13. package/dist/commands/browser.d.ts +1 -0
  14. package/dist/commands/browser.js +273 -0
  15. package/dist/commands/cleanup.d.ts +1 -0
  16. package/dist/commands/cleanup.js +24 -0
  17. package/dist/commands/emulator.d.ts +1 -0
  18. package/dist/commands/emulator.js +23 -0
  19. package/dist/commands/errors.d.ts +5 -0
  20. package/dist/commands/errors.js +10 -0
  21. package/dist/commands/index.d.ts +9 -0
  22. package/dist/commands/index.js +9 -0
  23. package/dist/commands/plugin.d.ts +6 -0
  24. package/dist/commands/plugin.js +289 -0
  25. package/dist/commands/runtime.d.ts +1 -0
  26. package/dist/commands/runtime.js +136 -0
  27. package/dist/commands/skills.d.ts +10 -0
  28. package/dist/commands/skills.js +80 -0
  29. package/dist/control-client.d.ts +163 -0
  30. package/dist/control-client.js +1028 -0
  31. package/dist/control-endpoint.d.ts +29 -0
  32. package/dist/control-endpoint.js +121 -0
  33. package/dist/desktop-browser-host-client.d.ts +44 -0
  34. package/dist/desktop-browser-host-client.js +430 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +2 -0
  37. package/dist/legacy-adapter.d.ts +7 -0
  38. package/dist/legacy-adapter.js +63 -0
  39. package/dist/run-cli.d.ts +1 -0
  40. package/dist/run-cli.js +62 -0
  41. package/dist/usage.d.ts +1 -0
  42. package/dist/usage.js +56 -0
  43. package/dist/version.d.ts +1 -0
  44. package/dist/version.js +8 -0
  45. package/package.json +52 -0
  46. package/skills/rynx-cli/SKILL.md +99 -0
  47. package/skills/rynx-cli/agents/openai.yaml +4 -0
@@ -0,0 +1,1028 @@
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 { DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, DAEMON_BROWSER_ARTIFACT_VERSION_PATH, DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonBrowserArtifactCleanResult, parseDaemonBrowserArtifactInstallResult, parseDaemonBrowserArtifactVersionResult, parseDaemonCleanupSessionsResult, parseDaemonChromeInspectionConfigureInput, parseDaemonChromeInspectionStatus, parseDaemonShutdownIfIdleResult, } from "@rynx-ai/protocol/control";
5
+ import { parseRemoteRuntimeRpcRequest, parseRemoteRuntimeRpcResponseForMethod, REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
6
+ import { parsePairingOffer, } from "@rynx-ai/protocol/direct-runtime";
7
+ 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";
8
+ import { parseRuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
9
+ import { PLUGIN_INSTALL_PREPARATIONS_PATH, parsePluginInstallCommitInput, parsePluginInstallCommitResult, parsePluginInstallPreparation, parsePluginInstallPrepareInput, parsePluginManagementItem, pluginInstallCommitPath, pluginInstallPreparationPath, } from "@rynx-ai/protocol/plugin-management";
10
+ import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
11
+ export { connectResidentDesktopBrowserHost, } from "./desktop-browser-host-client.js";
12
+ const IDENTITY_TIMEOUT_MS = 5_000;
13
+ const RUNTIME_STATUS_TIMEOUT_MS = 5_000;
14
+ const DAEMON_SHUTDOWN_TIMEOUT_MS = 5_000;
15
+ const PLUGIN_CLI_TIMEOUT_MS = 120_000;
16
+ const PLUGIN_CLI_BODY_MAX_BYTES = 512 * 1024;
17
+ const PLUGIN_CLI_STDIN_MAX_BYTES = 256 * 1024;
18
+ const PLUGIN_CLI_ARG_MAX_BYTES = 4 * 1024;
19
+ const PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES = 32 * 1024;
20
+ const PLUGIN_CLI_ARGS_MAX_COUNT = 64;
21
+ const IDENTITY_RESPONSE_MAX_BYTES = 16 * 1024;
22
+ const RUNTIME_STATUS_RESPONSE_MAX_BYTES = 64 * 1024;
23
+ const DAEMON_SHUTDOWN_RESPONSE_MAX_BYTES = 4 * 1024;
24
+ const REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES = 64 * 1024;
25
+ const RUNTIME_TARGET_RESPONSE_MAX_BYTES = 128 * 1024;
26
+ const RUNTIME_TARGET_REQUEST_MAX_BYTES = 64 * 1024;
27
+ const RUNTIME_TARGET_TIMEOUT_MS = 30_000;
28
+ const RESIDENT_RUNTIME_CALL_TIMEOUT_MS = 135_000;
29
+ const RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES = REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES;
30
+ const RUNTIME_BROWSER_BOOTSTRAP_TIMEOUT_MS = 5_000;
31
+ const RUNTIME_BROWSER_BOOTSTRAP_RESPONSE_MAX_BYTES = 16 * 1_024;
32
+ const RUNTIME_BROWSER_CONTEXT_MAX_BYTES = 4 * 1_024;
33
+ const RUNTIME_BROWSER_CONTEXT_PATH_MAX_CHARS = 4_096;
34
+ const PLUGIN_CLI_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
35
+ const PLUGIN_MANAGEMENT_TIMEOUT_MS = 30_000;
36
+ const PLUGIN_INSTALL_OPERATION_TIMEOUT_MS = 15 * 60_000;
37
+ const PLUGIN_MANAGEMENT_RESPONSE_MAX_BYTES = 256 * 1024;
38
+ const BROWSER_ARTIFACT_OPERATION_TIMEOUT_MS = 15 * 60_000;
39
+ const BROWSER_ARTIFACT_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
40
+ const MAINTENANCE_TIMEOUT_MS = 30_000;
41
+ const MAINTENANCE_RESPONSE_MAX_BYTES = 64 * 1024;
42
+ const CHROME_INSPECTION_TIMEOUT_MS = 30_000;
43
+ const CHROME_INSPECTION_RESPONSE_MAX_BYTES = 256 * 1024;
44
+ 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;
45
+ const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
46
+ /** Stable error returned by the resident daemon's typed Runtime gateway. */
47
+ export class ResidentRuntimeCallError extends Error {
48
+ code;
49
+ status;
50
+ outcome;
51
+ constructor(code, message, status, options) {
52
+ super(message, options);
53
+ this.code = code;
54
+ this.status = status;
55
+ this.name = "ResidentRuntimeCallError";
56
+ this.outcome = options?.outcome;
57
+ }
58
+ }
59
+ /** Connect to the resident daemon already owned by the App or standalone supervisor. */
60
+ export async function ensureResidentDaemon() {
61
+ const endpoint = await ensureDaemonControlEndpoint();
62
+ return { origin: endpoint.origin };
63
+ }
64
+ /** Read the stable public identity of the single resident daemon. */
65
+ export async function getResidentDaemonIdentity() {
66
+ const endpoint = await ensureDaemonControlEndpoint();
67
+ assertLoopbackOrigin(endpoint.origin);
68
+ const response = await fetch(`${endpoint.origin}/api/internal/daemon/identity`, {
69
+ headers: managementHeaders(endpoint.managementToken),
70
+ signal: AbortSignal.timeout(IDENTITY_TIMEOUT_MS),
71
+ });
72
+ const body = await readBoundedResponse(response, IDENTITY_RESPONSE_MAX_BYTES);
73
+ if (!response.ok)
74
+ throw daemonRequestError("daemon identity request", response.status, body);
75
+ const parsed = parseJsonObject(body, "daemon returned an invalid identity response");
76
+ if (!INSTALLATION_ID_PATTERN.test(readString(parsed.installationId)) ||
77
+ parsed.algorithm !== "Ed25519" ||
78
+ !/^[A-Za-z0-9_-]+$/.test(readString(parsed.publicKey)) ||
79
+ readString(parsed.publicKey).length > 512) {
80
+ throw new Error("daemon returned an invalid identity response");
81
+ }
82
+ return {
83
+ installationId: parsed.installationId,
84
+ algorithm: "Ed25519",
85
+ publicKey: parsed.publicKey,
86
+ };
87
+ }
88
+ /** Read the transport-independent status of the resident daemon process. */
89
+ export async function getResidentDaemonRuntimeStatus() {
90
+ const endpoint = await ensureDaemonControlEndpoint();
91
+ assertLoopbackOrigin(endpoint.origin);
92
+ const response = await fetch(`${endpoint.origin}/api/internal/daemon/runtime-status`, {
93
+ headers: managementHeaders(endpoint.managementToken),
94
+ signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
95
+ });
96
+ const body = await readBoundedResponse(response, RUNTIME_STATUS_RESPONSE_MAX_BYTES);
97
+ if (!response.ok)
98
+ throw daemonRequestError("daemon Runtime status request", response.status, body);
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(body);
102
+ }
103
+ catch {
104
+ throw new Error("daemon returned an invalid Remote Runtime status response");
105
+ }
106
+ const status = parseDaemonStatus(parsed);
107
+ if (!isDaemonCoreCompatible(status)) {
108
+ throw new Error(`resident daemon Remote Runtime Core protocol range ` +
109
+ `[${status.minimumCompatibleCoreVersion}, ${status.coreProtocolVersion}] is incompatible`);
110
+ }
111
+ return status;
112
+ }
113
+ /**
114
+ * Ask the resident daemon to perform one final activity check and stop only
115
+ * when idle. Busy is a normal result so the caller can ask the user to resolve
116
+ * the listed work and explicitly retry.
117
+ */
118
+ export async function shutdownResidentDaemonIfIdle() {
119
+ const endpoint = await ensureDaemonControlEndpoint();
120
+ assertLoopbackOrigin(endpoint.origin);
121
+ const response = await fetch(`${endpoint.origin}${DAEMON_SHUTDOWN_IF_IDLE_PATH}`, {
122
+ method: "POST",
123
+ headers: managementHeaders(endpoint.managementToken),
124
+ signal: AbortSignal.timeout(DAEMON_SHUTDOWN_TIMEOUT_MS),
125
+ });
126
+ const body = await readBoundedResponse(response, DAEMON_SHUTDOWN_RESPONSE_MAX_BYTES);
127
+ if (response.status !== 202 && response.status !== 409) {
128
+ throw daemonRequestError("daemon shutdown-if-idle request", response.status, body);
129
+ }
130
+ let result;
131
+ try {
132
+ result = parseDaemonShutdownIfIdleResult(JSON.parse(body));
133
+ }
134
+ catch (error) {
135
+ throw new Error(`daemon returned an invalid shutdown-if-idle response: ${messageOf(error)}`);
136
+ }
137
+ if (response.status === 202 && result.outcome !== "accepted" ||
138
+ response.status === 409 && result.outcome !== "busy") {
139
+ throw new Error("daemon returned a shutdown-if-idle outcome inconsistent with HTTP status");
140
+ }
141
+ return result;
142
+ }
143
+ export async function getResidentChromeInspectionStatus() {
144
+ const result = await localManagementJsonRequest(DAEMON_CHROME_INSPECTION_PATH, {
145
+ method: "GET",
146
+ timeoutMs: CHROME_INSPECTION_TIMEOUT_MS,
147
+ maxResponseBytes: CHROME_INSPECTION_RESPONSE_MAX_BYTES,
148
+ operation: "Chrome inspection status",
149
+ });
150
+ return parseDaemonChromeInspectionStatus(result);
151
+ }
152
+ export async function configureResidentChromeInspection(input) {
153
+ const normalized = parseDaemonChromeInspectionConfigureInput(input);
154
+ const result = await localManagementJsonRequest(DAEMON_CHROME_INSPECTION_PATH, {
155
+ method: "PUT",
156
+ body: normalized,
157
+ timeoutMs: CHROME_INSPECTION_TIMEOUT_MS,
158
+ maxResponseBytes: CHROME_INSPECTION_RESPONSE_MAX_BYTES,
159
+ operation: "Chrome inspection configuration",
160
+ });
161
+ return parseDaemonChromeInspectionStatus(result);
162
+ }
163
+ export async function installResidentBrowserArtifact(input) {
164
+ const result = await localManagementJsonRequest(DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, {
165
+ method: "POST",
166
+ body: input,
167
+ timeoutMs: BROWSER_ARTIFACT_OPERATION_TIMEOUT_MS,
168
+ maxResponseBytes: BROWSER_ARTIFACT_RESPONSE_MAX_BYTES,
169
+ operation: "Browser artifact install",
170
+ });
171
+ return parseDaemonBrowserArtifactInstallResult(result);
172
+ }
173
+ export async function updateResidentBrowserArtifact(input) {
174
+ const result = await localManagementJsonRequest(DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, {
175
+ method: "POST",
176
+ body: input,
177
+ timeoutMs: BROWSER_ARTIFACT_OPERATION_TIMEOUT_MS,
178
+ maxResponseBytes: BROWSER_ARTIFACT_RESPONSE_MAX_BYTES,
179
+ operation: "Browser artifact update",
180
+ });
181
+ return parseDaemonBrowserArtifactInstallResult(result);
182
+ }
183
+ export async function getResidentBrowserArtifactVersion() {
184
+ const result = await localManagementJsonRequest(DAEMON_BROWSER_ARTIFACT_VERSION_PATH, {
185
+ method: "GET",
186
+ timeoutMs: MAINTENANCE_TIMEOUT_MS,
187
+ maxResponseBytes: BROWSER_ARTIFACT_RESPONSE_MAX_BYTES,
188
+ operation: "Browser artifact version",
189
+ });
190
+ return parseDaemonBrowserArtifactVersionResult(result);
191
+ }
192
+ export async function cleanResidentBrowserArtifacts() {
193
+ const result = await localManagementJsonRequest(DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, {
194
+ method: "POST",
195
+ timeoutMs: MAINTENANCE_TIMEOUT_MS,
196
+ maxResponseBytes: BROWSER_ARTIFACT_RESPONSE_MAX_BYTES,
197
+ operation: "Browser artifact clean",
198
+ });
199
+ return parseDaemonBrowserArtifactCleanResult(result);
200
+ }
201
+ export async function cleanupResidentSessions(input = {}) {
202
+ const result = await localManagementJsonRequest(DAEMON_CLEANUP_SESSIONS_PATH, {
203
+ method: "POST",
204
+ body: input,
205
+ timeoutMs: MAINTENANCE_TIMEOUT_MS,
206
+ maxResponseBytes: MAINTENANCE_RESPONSE_MAX_BYTES,
207
+ operation: "Session cleanup",
208
+ });
209
+ return parseDaemonCleanupSessionsResult(result);
210
+ }
211
+ /** Create one pairing offer on the resident daemon for a selected reachable route. */
212
+ export async function createResidentRemoteRuntimePairingOffer(input = {}) {
213
+ const endpoint = await ensureDaemonControlEndpoint();
214
+ assertLoopbackOrigin(endpoint.origin);
215
+ const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/pairing-offers`, {
216
+ method: "POST",
217
+ headers: {
218
+ ...managementHeaders(endpoint.managementToken),
219
+ "content-type": "application/json",
220
+ },
221
+ body: JSON.stringify(input),
222
+ signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
223
+ });
224
+ const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
225
+ if (!response.ok)
226
+ throw daemonRequestError("pairing offer request", response.status, body);
227
+ try {
228
+ return parsePairingOffer(JSON.parse(body));
229
+ }
230
+ catch (error) {
231
+ throw new Error(`daemon returned an invalid Remote Runtime pairing offer: ${messageOf(error)}`);
232
+ }
233
+ }
234
+ /** List redacted client grants accepted by the resident daemon. */
235
+ export async function listResidentRemoteRuntimeClients() {
236
+ const endpoint = await ensureDaemonControlEndpoint();
237
+ assertLoopbackOrigin(endpoint.origin);
238
+ const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/clients`, {
239
+ headers: managementHeaders(endpoint.managementToken),
240
+ signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
241
+ });
242
+ const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
243
+ if (!response.ok)
244
+ throw daemonRequestError("Remote Runtime clients request", response.status, body);
245
+ const parsed = parseJsonObject(body, "daemon returned an invalid Remote Runtime clients response");
246
+ if (!Array.isArray(parsed.clients)) {
247
+ throw new Error("daemon returned an invalid Remote Runtime clients response");
248
+ }
249
+ return parsed.clients.map(parseRemoteRuntimeClientGrant);
250
+ }
251
+ /** Revoke one resident-daemon client grant and close its active channels. */
252
+ export async function revokeResidentRemoteRuntimeClient(grantId) {
253
+ if (!OPAQUE_ID_PATTERN.test(grantId) || grantId.length > 256) {
254
+ throw new Error("invalid Remote Runtime grant id");
255
+ }
256
+ const endpoint = await ensureDaemonControlEndpoint();
257
+ assertLoopbackOrigin(endpoint.origin);
258
+ const response = await fetch(`${endpoint.origin}/api/internal/remote-runtime/clients/${encodeURIComponent(grantId)}`, {
259
+ method: "DELETE",
260
+ headers: managementHeaders(endpoint.managementToken),
261
+ signal: AbortSignal.timeout(RUNTIME_STATUS_TIMEOUT_MS),
262
+ });
263
+ const body = await readBoundedResponse(response, REMOTE_RUNTIME_ADMIN_RESPONSE_MAX_BYTES);
264
+ if (!response.ok)
265
+ throw daemonRequestError("Remote Runtime client revoke", response.status, body);
266
+ const parsed = parseJsonObject(body, "daemon returned an invalid client revoke response");
267
+ if (parsed.revoked !== true ||
268
+ !Number.isSafeInteger(parsed.closedConnections) ||
269
+ parsed.closedConnections < 0) {
270
+ throw new Error("daemon returned an invalid client revoke response");
271
+ }
272
+ return { revoked: true, closedConnections: parsed.closedConnections };
273
+ }
274
+ /** List local and paired Runtime targets through the resident daemon BFF. */
275
+ export async function listResidentRuntimeTargets() {
276
+ const response = await runtimeTargetRequest("/api/internal/runtimes", { method: "GET" });
277
+ const parsed = parseJsonObject(response, "daemon returned an invalid Runtime target list");
278
+ if (!Array.isArray(parsed.runtimes)) {
279
+ throw new Error("daemon returned an invalid Runtime target list");
280
+ }
281
+ return parsed.runtimes.map(parseRuntimeTargetView);
282
+ }
283
+ /** Pair a target; the offer remains inside the loopback management request. */
284
+ export async function pairResidentRuntimeTarget(offer, displayName) {
285
+ const body = JSON.stringify({ offer, ...(displayName ? { displayName } : {}) });
286
+ if (Buffer.byteLength(body, "utf8") > RUNTIME_TARGET_REQUEST_MAX_BYTES) {
287
+ throw new Error(`Runtime target pairing request exceeds ${RUNTIME_TARGET_REQUEST_MAX_BYTES} bytes`);
288
+ }
289
+ const response = await runtimeTargetRequest("/api/internal/runtimes/pair", {
290
+ method: "POST",
291
+ body,
292
+ });
293
+ return parseRuntimeTargetResult(response, "pairing");
294
+ }
295
+ export async function testResidentRuntimeTarget(selector) {
296
+ const normalized = validRuntimeSelector(selector);
297
+ const response = await runtimeTargetRequest(`/api/internal/runtimes/${encodeURIComponent(normalized)}/test`, { method: "POST", body: "{}" });
298
+ return parseRuntimeTargetResult(response, "test");
299
+ }
300
+ export async function forgetResidentRuntimeTarget(daemonId) {
301
+ const normalized = validRuntimeSelector(daemonId);
302
+ const response = await runtimeTargetRequest(`/api/internal/runtimes/${encodeURIComponent(normalized)}`, { method: "DELETE" });
303
+ const parsed = parseJsonObject(response, "daemon returned an invalid Runtime forget response");
304
+ if (parsed.forgotten !== true) {
305
+ throw new Error("daemon returned an invalid Runtime forget response");
306
+ }
307
+ return { forgotten: true };
308
+ }
309
+ /**
310
+ * Invoke one typed Runtime method through the single resident daemon.
311
+ *
312
+ * The short-lived CLI process never reads Direct credentials or opens a
313
+ * remote socket. The resident daemon owns target resolution, Local/Direct
314
+ * connection reuse, authorization and no-retry outcome semantics.
315
+ */
316
+ export async function callResidentRuntime(selectorInput, method, params, options = {}) {
317
+ let selector;
318
+ try {
319
+ selector = validRuntimeSelector(selectorInput);
320
+ }
321
+ catch (error) {
322
+ throw new ResidentRuntimeCallError("invalid_target", "Runtime target selector is invalid", undefined, { cause: error, outcome: "not_started" });
323
+ }
324
+ let request;
325
+ try {
326
+ request = parseRemoteRuntimeRpcRequest({
327
+ type: "rpc.request",
328
+ id: "resident-runtime-call",
329
+ method,
330
+ params,
331
+ });
332
+ }
333
+ catch (error) {
334
+ throw new ResidentRuntimeCallError("invalid_request", `Runtime ${method} parameters are invalid`, undefined, { cause: error, outcome: "not_started" });
335
+ }
336
+ const body = JSON.stringify({ method: request.method, params: request.params });
337
+ if (Buffer.byteLength(body, "utf8") > REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES) {
338
+ throw new ResidentRuntimeCallError("invalid_request", `Runtime request exceeds ${REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES} bytes`, undefined, { outcome: "not_started" });
339
+ }
340
+ if (options.signal?.aborted) {
341
+ throw new ResidentRuntimeCallError("cancelled", "Runtime call was cancelled before it started", undefined, { cause: options.signal.reason, outcome: "not_started" });
342
+ }
343
+ const timeoutSignal = AbortSignal.timeout(RESIDENT_RUNTIME_CALL_TIMEOUT_MS);
344
+ const signal = options.signal
345
+ ? AbortSignal.any([options.signal, timeoutSignal])
346
+ : timeoutSignal;
347
+ let endpoint;
348
+ try {
349
+ endpoint = await ensureDaemonControlEndpoint({ signal });
350
+ signal.throwIfAborted();
351
+ }
352
+ catch (error) {
353
+ if (options.signal?.aborted) {
354
+ throw new ResidentRuntimeCallError("cancelled", "Runtime call was cancelled before it started", undefined, { cause: error, outcome: "not_started" });
355
+ }
356
+ throw new ResidentRuntimeCallError(timeoutSignal.aborted ? "deadline_exceeded" : "unreachable", timeoutSignal.aborted
357
+ ? "Resident daemon discovery exceeded the Runtime call deadline"
358
+ : "Resident daemon is unavailable", undefined, { cause: error, outcome: "not_started" });
359
+ }
360
+ try {
361
+ assertLoopbackOrigin(endpoint.origin);
362
+ }
363
+ catch (error) {
364
+ throw new ResidentRuntimeCallError("protocol_error", "Resident daemon control endpoint is not loopback-only", undefined, { cause: error, outcome: "not_started" });
365
+ }
366
+ let response;
367
+ try {
368
+ response = await fetch(`${endpoint.origin}/api/internal/runtimes/${encodeURIComponent(selector)}/rpc`, {
369
+ method: "POST",
370
+ headers: {
371
+ ...managementHeaders(endpoint.managementToken),
372
+ "content-type": "application/json",
373
+ },
374
+ body,
375
+ signal,
376
+ });
377
+ }
378
+ catch (error) {
379
+ const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
380
+ if (options.signal?.aborted) {
381
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "cancelled", neverRetry
382
+ ? `Runtime may have applied ${method} before local cancellation`
383
+ : `Runtime ${method} was cancelled`, undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
384
+ }
385
+ if (isRequestTimeout(error) || timeoutSignal.aborted) {
386
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "deadline_exceeded", neverRetry
387
+ ? `Runtime may have applied ${method} before the local request timed out`
388
+ : `Runtime ${method} exceeded the local request deadline`, undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
389
+ }
390
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "unreachable", neverRetry
391
+ ? `Runtime may have applied ${method} before the local connection failed`
392
+ : "Resident Runtime request could not reach the selected target", undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
393
+ }
394
+ const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
395
+ let responseBody;
396
+ try {
397
+ responseBody = await readBoundedResponse(response, RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES);
398
+ }
399
+ catch (error) {
400
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
401
+ ? `Runtime may have applied ${method}, but its response was incomplete`
402
+ : "daemon returned an unreadable Runtime call response", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
403
+ }
404
+ if (!response.ok)
405
+ throw parseResidentRuntimeCallError(response.status, responseBody);
406
+ try {
407
+ const envelope = parseJsonObject(responseBody, "daemon returned an invalid Runtime call response");
408
+ if (Object.keys(envelope).length !== 1 || !Object.hasOwn(envelope, "result")) {
409
+ throw new Error("Runtime result envelope is invalid");
410
+ }
411
+ const parsed = parseRemoteRuntimeRpcResponseForMethod({
412
+ type: "rpc.result",
413
+ id: "resident-runtime-call",
414
+ result: envelope.result,
415
+ }, method);
416
+ if (parsed.type !== "rpc.result")
417
+ throw new Error("expected a Runtime result");
418
+ return parsed.result;
419
+ }
420
+ catch (error) {
421
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
422
+ ? `Runtime may have applied ${method}, but its result was invalid`
423
+ : "daemon returned an invalid Runtime call result", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
424
+ }
425
+ }
426
+ /** Read the current managed Session credential without exposing it in output. */
427
+ export async function readManagedRuntimeBrowserCredential(env = process.env) {
428
+ const contextFile = env[RUNTIME_BROWSER_CONTEXT_FILE_ENV];
429
+ if (!contextFile) {
430
+ try {
431
+ return parseRuntimeBrowserBootstrapCredential({
432
+ sessionId: env[RUNTIME_BROWSER_SESSION_ID_ENV],
433
+ capability: env[RUNTIME_BROWSER_CAPABILITY_ENV],
434
+ });
435
+ }
436
+ catch (error) {
437
+ throw new Error("This command must run inside a managed Rynx Session", { cause: error });
438
+ }
439
+ }
440
+ if (contextFile.length > RUNTIME_BROWSER_CONTEXT_PATH_MAX_CHARS ||
441
+ contextFile.includes("\0") ||
442
+ !isAbsolute(contextFile)) {
443
+ throw new Error("Managed Runtime Browser context path is invalid");
444
+ }
445
+ const file = await open(contextFile, "r");
446
+ try {
447
+ const stat = await file.stat();
448
+ if (!stat.isFile() || stat.size > RUNTIME_BROWSER_CONTEXT_MAX_BYTES) {
449
+ throw new Error("Managed Runtime Browser context file is invalid");
450
+ }
451
+ const raw = await file.readFile({ encoding: "utf8" });
452
+ if (Buffer.byteLength(raw, "utf8") > RUNTIME_BROWSER_CONTEXT_MAX_BYTES) {
453
+ throw new Error("Managed Runtime Browser context file is too large");
454
+ }
455
+ try {
456
+ return parseRuntimeBrowserBootstrapCredential(JSON.parse(raw));
457
+ }
458
+ catch (error) {
459
+ throw new Error("Managed Runtime Browser context is invalid", { cause: error });
460
+ }
461
+ }
462
+ finally {
463
+ await file.close();
464
+ }
465
+ }
466
+ /** Return undefined only when the process has no managed Session context at all. */
467
+ export async function readOptionalManagedRuntimeBrowserCredential(env = process.env) {
468
+ if (env[RUNTIME_BROWSER_CONTEXT_FILE_ENV] === undefined &&
469
+ env[RUNTIME_BROWSER_SESSION_ID_ENV] === undefined &&
470
+ env[RUNTIME_BROWSER_CAPABILITY_ENV] === undefined)
471
+ return undefined;
472
+ return readManagedRuntimeBrowserCredential(env);
473
+ }
474
+ const MANAGED_BROWSER_RPC_METHODS = new Set([
475
+ "browser.state.get",
476
+ "browser.open",
477
+ "browser.close",
478
+ "browser.page.create",
479
+ "browser.page.close",
480
+ "browser.page.activate",
481
+ "browser.page.navigate",
482
+ "browser.page.back",
483
+ "browser.page.forward",
484
+ "browser.page.reload",
485
+ ]);
486
+ /**
487
+ * Invoke Browser control for the caller's own managed Session. Unlike the
488
+ * operator Runtime gateway, this path never reads or sends daemon management
489
+ * authority and cannot select a remote Runtime or another Session.
490
+ */
491
+ export async function callManagedRuntimeBrowser(credential, method, params, options = {}) {
492
+ const validatedCredential = parseRuntimeBrowserBootstrapCredential(credential);
493
+ let request;
494
+ try {
495
+ request = parseRemoteRuntimeRpcRequest({
496
+ type: "rpc.request",
497
+ id: "managed-session-browser",
498
+ method,
499
+ params,
500
+ });
501
+ }
502
+ catch (error) {
503
+ throw new ResidentRuntimeCallError("invalid_request", `Runtime ${method} parameters are invalid`, undefined, { cause: error, outcome: "not_started" });
504
+ }
505
+ if (!MANAGED_BROWSER_RPC_METHODS.has(request.method) ||
506
+ !("sessionId" in request.params) ||
507
+ request.params.sessionId !== validatedCredential.sessionId) {
508
+ throw new ResidentRuntimeCallError("unauthorized", "Managed Browser control is limited to the caller's own Session", undefined, { outcome: "not_started" });
509
+ }
510
+ const timeoutSignal = AbortSignal.timeout(RESIDENT_RUNTIME_CALL_TIMEOUT_MS);
511
+ const signal = options.signal
512
+ ? AbortSignal.any([options.signal, timeoutSignal])
513
+ : timeoutSignal;
514
+ const endpoint = await ensureDaemonControlEndpoint({ signal });
515
+ assertLoopbackOrigin(endpoint.origin);
516
+ let response;
517
+ try {
518
+ response = await fetch(`${endpoint.origin}${RUNTIME_BROWSER_RPC_PATH}`, {
519
+ method: "POST",
520
+ headers: {
521
+ "content-type": "application/json",
522
+ [RUNTIME_BROWSER_SESSION_ID_HEADER]: validatedCredential.sessionId,
523
+ [RUNTIME_BROWSER_CAPABILITY_HEADER]: validatedCredential.capability,
524
+ },
525
+ body: JSON.stringify({ method: request.method, params: request.params }),
526
+ signal,
527
+ });
528
+ }
529
+ catch (error) {
530
+ const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
531
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : timeoutSignal.aborted ? "deadline_exceeded" : "unreachable", neverRetry
532
+ ? `Runtime may have applied ${method} before the local connection failed`
533
+ : timeoutSignal.aborted
534
+ ? `Runtime ${method} exceeded the local request deadline`
535
+ : "Resident daemon is unavailable", undefined, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
536
+ }
537
+ const neverRetry = REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never";
538
+ let body;
539
+ try {
540
+ body = await readBoundedResponse(response, RESIDENT_RUNTIME_CALL_RESPONSE_MAX_BYTES);
541
+ }
542
+ catch (error) {
543
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
544
+ ? `Runtime may have applied ${method}, but its response was incomplete`
545
+ : "daemon returned an unreadable Runtime call response", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
546
+ }
547
+ if (!response.ok)
548
+ throw parseResidentRuntimeCallError(response.status, body);
549
+ try {
550
+ const envelope = parseJsonObject(body, "daemon returned an invalid Runtime call response");
551
+ if (Object.keys(envelope).length !== 1 || !Object.hasOwn(envelope, "result")) {
552
+ throw new Error("Runtime result envelope is invalid");
553
+ }
554
+ const parsed = parseRemoteRuntimeRpcResponseForMethod({
555
+ type: "rpc.result",
556
+ id: "managed-session-browser",
557
+ result: envelope.result,
558
+ }, method);
559
+ if (parsed.type !== "rpc.result")
560
+ throw new Error("expected a Runtime result");
561
+ return parsed.result;
562
+ }
563
+ catch (error) {
564
+ throw new ResidentRuntimeCallError(neverRetry ? "outcome_unknown" : "protocol_error", neverRetry
565
+ ? `Runtime may have applied ${method}, but its result was invalid`
566
+ : "daemon returned an invalid Runtime call result", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
567
+ }
568
+ }
569
+ /** Resolve native CDP for the caller's own Session on this Runtime only. */
570
+ export async function getResidentRuntimeLocalBrowserEndpoint(options = {}) {
571
+ return (await getResidentRuntimeLocalBrowserAutomationAccess(options)).descriptor;
572
+ }
573
+ /**
574
+ * Resolve CDP together with the Runtime-scoped MAC key used for opaque
575
+ * automation refs. Keep the key in-process; it must never be printed.
576
+ */
577
+ export async function getResidentRuntimeLocalBrowserAutomationAccess(options = {}) {
578
+ const credential = options.sessionId === undefined
579
+ ? await readManagedRuntimeBrowserCredential(options.env)
580
+ : undefined;
581
+ const explicitSessionId = options.sessionId === undefined
582
+ ? undefined
583
+ : parseRuntimeBrowserStateGetParams({ sessionId: options.sessionId }).sessionId;
584
+ const timeoutSignal = AbortSignal.timeout(RUNTIME_BROWSER_BOOTSTRAP_TIMEOUT_MS);
585
+ const signal = options.signal
586
+ ? AbortSignal.any([options.signal, timeoutSignal])
587
+ : timeoutSignal;
588
+ const endpoint = await ensureDaemonControlEndpoint({ signal });
589
+ assertLoopbackOrigin(endpoint.origin);
590
+ const path = credential
591
+ ? RUNTIME_BROWSER_BOOTSTRAP_PATH
592
+ : `${RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX}/${encodeURIComponent(explicitSessionId)}/endpoint`;
593
+ const response = await fetch(`${endpoint.origin}${path}`, {
594
+ headers: credential
595
+ ? {
596
+ [RUNTIME_BROWSER_SESSION_ID_HEADER]: credential.sessionId,
597
+ [RUNTIME_BROWSER_CAPABILITY_HEADER]: credential.capability,
598
+ }
599
+ : managementHeaders(endpoint.managementToken),
600
+ signal,
601
+ });
602
+ const body = await readBoundedResponse(response, RUNTIME_BROWSER_BOOTSTRAP_RESPONSE_MAX_BYTES);
603
+ if (!response.ok) {
604
+ throw daemonRequestError("Runtime Browser endpoint request", response.status, body);
605
+ }
606
+ let descriptor;
607
+ try {
608
+ descriptor = parseRuntimeBrowserEndpointDescriptor(JSON.parse(body));
609
+ }
610
+ catch (error) {
611
+ throw new Error("daemon returned an invalid Runtime Browser endpoint", { cause: error });
612
+ }
613
+ if (descriptor.sessionId !== (credential?.sessionId ?? explicitSessionId)) {
614
+ throw new Error("daemon returned a Runtime Browser endpoint for another Session");
615
+ }
616
+ return {
617
+ descriptor,
618
+ referenceKey: endpoint.managementToken,
619
+ };
620
+ }
621
+ /** Invoke one command declared by an installed plugin through the resident daemon. */
622
+ export async function invokeResidentPluginCommand(pluginId, args, options = {}) {
623
+ validatePluginInvocation(pluginId, args, options.stdin);
624
+ const requestSignal = options.signal
625
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(PLUGIN_CLI_TIMEOUT_MS)])
626
+ : AbortSignal.timeout(PLUGIN_CLI_TIMEOUT_MS);
627
+ requestSignal.throwIfAborted();
628
+ const endpoint = await ensureDaemonControlEndpoint({ signal: requestSignal });
629
+ requestSignal.throwIfAborted();
630
+ assertLoopbackOrigin(endpoint.origin);
631
+ const requestBody = JSON.stringify({
632
+ args,
633
+ ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
634
+ });
635
+ if (Buffer.byteLength(requestBody, "utf8") > PLUGIN_CLI_BODY_MAX_BYTES) {
636
+ throw new Error(`plugin command request exceeds ${PLUGIN_CLI_BODY_MAX_BYTES} bytes`);
637
+ }
638
+ const response = await fetch(`${endpoint.origin}/api/internal/plugins/${encodeURIComponent(pluginId)}/cli`, {
639
+ method: "POST",
640
+ headers: {
641
+ ...managementHeaders(endpoint.managementToken),
642
+ "content-type": "application/json",
643
+ },
644
+ body: requestBody,
645
+ signal: requestSignal,
646
+ });
647
+ const body = await readBoundedResponse(response, PLUGIN_CLI_RESPONSE_MAX_BYTES);
648
+ if (!response.ok)
649
+ throw daemonRequestError("plugin command request", response.status, body);
650
+ const parsed = parseJsonObject(body, "daemon returned an invalid plugin command response");
651
+ if (!Number.isInteger(parsed.code) ||
652
+ parsed.code < 0 ||
653
+ parsed.code > 255 ||
654
+ typeof parsed.stdout !== "string" ||
655
+ typeof parsed.stderr !== "string" ||
656
+ typeof parsed.truncated !== "boolean") {
657
+ throw new Error("daemon returned an invalid plugin command response");
658
+ }
659
+ return {
660
+ code: parsed.code,
661
+ stdout: parsed.stdout,
662
+ stderr: parsed.stderr,
663
+ truncated: parsed.truncated,
664
+ };
665
+ }
666
+ /** Read the daemon-owned plugin registry without exposing package paths/specs. */
667
+ export async function listResidentPlugins() {
668
+ const parsed = await pluginManagementRequest("/api/internal/plugins", { method: "GET" });
669
+ if (!Array.isArray(parsed.plugins)) {
670
+ throw new Error("daemon returned an invalid plugin list");
671
+ }
672
+ return parsed.plugins.map(parsePluginManagementItem);
673
+ }
674
+ /** Enable or disable one installed plugin through authenticated management RPC. */
675
+ export async function setResidentPluginEnabled(pluginId, enabled) {
676
+ validatePluginId(pluginId);
677
+ const parsed = await pluginManagementRequest(`/api/internal/plugins/${encodeURIComponent(pluginId)}/${enabled ? "enable" : "disable"}`, { method: "POST" });
678
+ return parsePluginManagementItem(parsed.plugin);
679
+ }
680
+ /** Remove one plugin registry entry through authenticated management RPC. */
681
+ export async function uninstallResidentPlugin(pluginId) {
682
+ validatePluginId(pluginId);
683
+ const parsed = await pluginManagementRequest(`/api/internal/plugins/${encodeURIComponent(pluginId)}`, { method: "DELETE" });
684
+ if (parsed.uninstalled !== true || parsed.pluginId !== pluginId) {
685
+ throw new Error("daemon returned an invalid plugin uninstall response");
686
+ }
687
+ return { uninstalled: true, pluginId };
688
+ }
689
+ /** Reload one plugin runtime after a durable registry mutation. */
690
+ export async function reloadResidentPluginRuntime(pluginId) {
691
+ validatePluginId(pluginId);
692
+ return pluginManagementRequest(`/api/internal/plugin-runtimes/${encodeURIComponent(pluginId)}/reload`, { method: "POST" });
693
+ }
694
+ /** Materialize and hold one exact plugin artifact in daemon-owned staging. */
695
+ export async function prepareResidentPluginInstallation(input, options = {}) {
696
+ const validated = parsePluginInstallPrepareInput(input);
697
+ const response = await pluginInstallRequest(PLUGIN_INSTALL_PREPARATIONS_PATH, {
698
+ method: "POST",
699
+ body: JSON.stringify(validated),
700
+ signal: options.signal,
701
+ });
702
+ return parsePluginInstallPreparation(response);
703
+ }
704
+ /** Consume one preparation token and promote only its already-approved artifact. */
705
+ export async function commitResidentPluginInstallation(token, input, options = {}) {
706
+ const validated = parsePluginInstallCommitInput(input);
707
+ const response = await pluginInstallRequest(pluginInstallCommitPath(token), {
708
+ method: "POST",
709
+ body: JSON.stringify(validated),
710
+ signal: options.signal,
711
+ });
712
+ return parsePluginInstallCommitResult(response);
713
+ }
714
+ /** Best-effort early release when the user declines a prepared artifact. */
715
+ export async function cancelResidentPluginInstallation(token) {
716
+ const parsed = await pluginManagementRequest(pluginInstallPreparationPath(token), {
717
+ method: "DELETE",
718
+ });
719
+ if (typeof parsed.cancelled !== "boolean") {
720
+ throw new Error("daemon returned an invalid plugin preparation cancellation");
721
+ }
722
+ return parsed.cancelled;
723
+ }
724
+ function managementHeaders(token) {
725
+ return { "x-rynx-management-token": token };
726
+ }
727
+ function assertLoopbackOrigin(origin) {
728
+ const hostname = new URL(origin).hostname;
729
+ if (hostname !== "127.0.0.1" && hostname !== "[::1]" && hostname !== "::1") {
730
+ throw new Error("resident daemon control endpoint is not loopback-only");
731
+ }
732
+ }
733
+ function validatePluginInvocation(pluginId, args, stdin) {
734
+ if (!PLUGIN_ID_PATTERN.test(pluginId))
735
+ throw new Error("invalid plugin id");
736
+ if (args.length === 0)
737
+ throw new Error("plugin command args must not be empty");
738
+ if (args.length > PLUGIN_CLI_ARGS_MAX_COUNT) {
739
+ throw new Error(`plugin command args exceed ${PLUGIN_CLI_ARGS_MAX_COUNT} entries`);
740
+ }
741
+ let totalBytes = 0;
742
+ for (const arg of args) {
743
+ if (typeof arg !== "string" || arg.includes("\0")) {
744
+ throw new Error("plugin command args must be strings without NUL bytes");
745
+ }
746
+ const bytes = Buffer.byteLength(arg, "utf8");
747
+ if (bytes > PLUGIN_CLI_ARG_MAX_BYTES) {
748
+ throw new Error(`plugin command arg exceeds ${PLUGIN_CLI_ARG_MAX_BYTES} bytes`);
749
+ }
750
+ totalBytes += bytes;
751
+ }
752
+ if (totalBytes > PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES) {
753
+ throw new Error(`plugin command args exceed ${PLUGIN_CLI_ARGS_TOTAL_MAX_BYTES} bytes`);
754
+ }
755
+ if (stdin !== undefined && typeof stdin !== "string")
756
+ throw new Error("plugin command stdin must be a string");
757
+ if (stdin !== undefined && Buffer.byteLength(stdin, "utf8") > PLUGIN_CLI_STDIN_MAX_BYTES) {
758
+ throw new Error(`plugin command stdin exceeds ${PLUGIN_CLI_STDIN_MAX_BYTES} bytes`);
759
+ }
760
+ }
761
+ function validatePluginId(pluginId) {
762
+ if (!PLUGIN_ID_PATTERN.test(pluginId))
763
+ throw new Error("invalid plugin id");
764
+ }
765
+ async function localManagementJsonRequest(pathname, input) {
766
+ const endpoint = await ensureDaemonControlEndpoint();
767
+ assertLoopbackOrigin(endpoint.origin);
768
+ const body = input.body === undefined ? undefined : JSON.stringify(input.body);
769
+ const response = await fetch(`${endpoint.origin}${pathname}`, {
770
+ method: input.method,
771
+ headers: {
772
+ ...managementHeaders(endpoint.managementToken),
773
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
774
+ },
775
+ ...(body === undefined ? {} : { body }),
776
+ signal: AbortSignal.timeout(input.timeoutMs),
777
+ });
778
+ const responseBody = await readBoundedResponse(response, input.maxResponseBytes);
779
+ if (!response.ok) {
780
+ throw daemonRequestError(`${input.operation} request`, response.status, responseBody);
781
+ }
782
+ return parseJsonObject(responseBody, `daemon returned an invalid ${input.operation.toLowerCase()} response`);
783
+ }
784
+ async function pluginManagementRequest(pathname, input) {
785
+ const endpoint = await ensureDaemonControlEndpoint();
786
+ assertLoopbackOrigin(endpoint.origin);
787
+ const response = await fetch(`${endpoint.origin}${pathname}`, {
788
+ method: input.method,
789
+ headers: managementHeaders(endpoint.managementToken),
790
+ signal: AbortSignal.timeout(PLUGIN_MANAGEMENT_TIMEOUT_MS),
791
+ });
792
+ const body = await readBoundedResponse(response, PLUGIN_MANAGEMENT_RESPONSE_MAX_BYTES);
793
+ if (!response.ok)
794
+ throw daemonRequestError("plugin management request", response.status, body);
795
+ return parseJsonObject(body, "daemon returned an invalid plugin management response");
796
+ }
797
+ async function pluginInstallRequest(pathname, input) {
798
+ const timeoutSignal = AbortSignal.timeout(PLUGIN_INSTALL_OPERATION_TIMEOUT_MS);
799
+ const signal = input.signal
800
+ ? AbortSignal.any([input.signal, timeoutSignal])
801
+ : timeoutSignal;
802
+ const endpoint = await ensureDaemonControlEndpoint({ signal });
803
+ assertLoopbackOrigin(endpoint.origin);
804
+ const response = await fetch(`${endpoint.origin}${pathname}`, {
805
+ method: input.method,
806
+ headers: {
807
+ ...managementHeaders(endpoint.managementToken),
808
+ "content-type": "application/json",
809
+ },
810
+ body: input.body,
811
+ signal,
812
+ });
813
+ const body = await readBoundedResponse(response, PLUGIN_MANAGEMENT_RESPONSE_MAX_BYTES);
814
+ if (!response.ok)
815
+ throw daemonRequestError("plugin installation request", response.status, body);
816
+ return parseJsonObject(body, "daemon returned an invalid plugin installation response");
817
+ }
818
+ async function readBoundedResponse(response, maxBytes) {
819
+ if (!response.body)
820
+ return "";
821
+ const reader = response.body.getReader();
822
+ const chunks = [];
823
+ let bytes = 0;
824
+ try {
825
+ while (true) {
826
+ const { done, value } = await reader.read();
827
+ if (done)
828
+ break;
829
+ bytes += value.byteLength;
830
+ if (bytes > maxBytes) {
831
+ await reader.cancel();
832
+ throw new Error(`daemon response exceeds ${maxBytes} bytes`);
833
+ }
834
+ chunks.push(Buffer.from(value));
835
+ }
836
+ }
837
+ finally {
838
+ reader.releaseLock();
839
+ }
840
+ return Buffer.concat(chunks, bytes).toString("utf8");
841
+ }
842
+ function parseJsonObject(body, message) {
843
+ try {
844
+ const parsed = JSON.parse(body);
845
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
846
+ return parsed;
847
+ }
848
+ }
849
+ catch {
850
+ // Fall through to the stable protocol error below.
851
+ }
852
+ throw new Error(message);
853
+ }
854
+ function daemonRequestError(operation, status, body) {
855
+ let detail = body.trim().slice(0, 2_000);
856
+ try {
857
+ const parsed = JSON.parse(body);
858
+ if (typeof parsed.error === "string")
859
+ detail = parsed.error.slice(0, 2_000);
860
+ }
861
+ catch {
862
+ // A bounded plain-text error is still useful for local diagnostics.
863
+ }
864
+ return new Error(`${operation} failed (${status})${detail ? `: ${detail}` : ""}`);
865
+ }
866
+ const RESIDENT_RUNTIME_CALL_ERROR_CODES = new Set([
867
+ "invalid_target",
868
+ "invalid_request",
869
+ "not_found",
870
+ "conflict",
871
+ "failed_precondition",
872
+ "method_not_found",
873
+ "deadline_exceeded",
874
+ "unreachable",
875
+ "tls_error",
876
+ "identity_mismatch",
877
+ "incompatible",
878
+ "authentication_failed",
879
+ "protocol_error",
880
+ "operation_failed",
881
+ "outcome_unknown",
882
+ "cancelled",
883
+ "unauthorized",
884
+ "closed",
885
+ "internal",
886
+ "internal_error",
887
+ ]);
888
+ function parseResidentRuntimeCallError(status, body) {
889
+ let code = "internal_error";
890
+ let outcome;
891
+ try {
892
+ const parsed = JSON.parse(body);
893
+ if (typeof parsed.error === "string" &&
894
+ RESIDENT_RUNTIME_CALL_ERROR_CODES.has(parsed.error)) {
895
+ code = parsed.error;
896
+ }
897
+ if (parsed.outcome === "not_started" || parsed.outcome === "unknown") {
898
+ outcome = parsed.outcome;
899
+ }
900
+ }
901
+ catch {
902
+ // Keep the stable internal_error fallback; never expose a daemon body.
903
+ }
904
+ return new ResidentRuntimeCallError(code, `Runtime call failed (${status}): ${code}`, status, { ...(outcome ? { outcome } : code === "outcome_unknown" ? { outcome: "unknown" } : {}) });
905
+ }
906
+ function isRequestTimeout(error) {
907
+ return error instanceof Error &&
908
+ (error.name === "TimeoutError" || error.name === "AbortError");
909
+ }
910
+ function readString(value) {
911
+ return typeof value === "string" ? value : "";
912
+ }
913
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/;
914
+ function parseRemoteRuntimeClientGrant(value) {
915
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
916
+ throw new Error("daemon returned an invalid Remote Runtime client grant");
917
+ }
918
+ const record = value;
919
+ for (const field of ["grantId", "clientId"]) {
920
+ if (typeof record[field] !== "string" ||
921
+ record[field].length > 256 ||
922
+ !OPAQUE_ID_PATTERN.test(record[field])) {
923
+ throw new Error("daemon returned an invalid Remote Runtime client grant");
924
+ }
925
+ }
926
+ if (record.clientLabel !== null && typeof record.clientLabel !== "string" ||
927
+ typeof record.clientKeyFingerprint !== "string" ||
928
+ !record.clientKeyFingerprint.startsWith("SHA256:") ||
929
+ !Array.isArray(record.authorities) ||
930
+ record.authorities.length !== 1 ||
931
+ record.authorities[0] !== "daemon.full" ||
932
+ typeof record.createdAt !== "string" ||
933
+ !Number.isFinite(Date.parse(record.createdAt)) ||
934
+ record.revokedAt !== null &&
935
+ (typeof record.revokedAt !== "string" || !Number.isFinite(Date.parse(record.revokedAt))) ||
936
+ record.lastUsedAt !== null &&
937
+ (typeof record.lastUsedAt !== "string" || !Number.isFinite(Date.parse(record.lastUsedAt)))) {
938
+ throw new Error("daemon returned an invalid Remote Runtime client grant");
939
+ }
940
+ return {
941
+ grantId: record.grantId,
942
+ clientId: record.clientId,
943
+ clientLabel: record.clientLabel,
944
+ clientKeyFingerprint: record.clientKeyFingerprint,
945
+ authorities: ["daemon.full"],
946
+ createdAt: record.createdAt,
947
+ revokedAt: record.revokedAt,
948
+ lastUsedAt: record.lastUsedAt,
949
+ };
950
+ }
951
+ async function runtimeTargetRequest(pathname, input) {
952
+ const endpoint = await ensureDaemonControlEndpoint();
953
+ assertLoopbackOrigin(endpoint.origin);
954
+ const response = await fetch(`${endpoint.origin}${pathname}`, {
955
+ method: input.method,
956
+ headers: {
957
+ ...managementHeaders(endpoint.managementToken),
958
+ ...(input.body === undefined ? {} : { "content-type": "application/json" }),
959
+ },
960
+ ...(input.body === undefined ? {} : { body: input.body }),
961
+ signal: AbortSignal.timeout(RUNTIME_TARGET_TIMEOUT_MS),
962
+ });
963
+ const body = await readBoundedResponse(response, RUNTIME_TARGET_RESPONSE_MAX_BYTES);
964
+ if (!response.ok)
965
+ throw daemonRequestError("Runtime target request", response.status, body);
966
+ return body;
967
+ }
968
+ function parseRuntimeTargetResult(body, operation) {
969
+ const parsed = parseJsonObject(body, `daemon returned an invalid Runtime target ${operation} response`);
970
+ try {
971
+ return {
972
+ target: parseRuntimeTargetView(parsed.target),
973
+ status: parseDaemonStatus(parsed.status),
974
+ };
975
+ }
976
+ catch (error) {
977
+ throw new Error(`daemon returned an invalid Runtime target ${operation} response: ${messageOf(error)}`);
978
+ }
979
+ }
980
+ function parseRuntimeTargetView(value) {
981
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
982
+ throw new Error("invalid Runtime target view");
983
+ }
984
+ const record = value;
985
+ const selector = validRuntimeSelector(record.selector);
986
+ const daemonId = validRuntimeSelector(record.daemonId);
987
+ if (typeof record.displayName !== "string" ||
988
+ record.displayName.length === 0 ||
989
+ record.displayName.length > 128 ||
990
+ (record.binding !== "local" && record.binding !== "direct") ||
991
+ (record.endpoint !== undefined && typeof record.endpoint !== "string") ||
992
+ (record.identityKeyFingerprint !== undefined &&
993
+ (typeof record.identityKeyFingerprint !== "string" ||
994
+ !record.identityKeyFingerprint.startsWith("SHA256:"))) ||
995
+ (record.createdAt !== undefined &&
996
+ (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))))) {
997
+ throw new Error("invalid Runtime target view");
998
+ }
999
+ if (record.binding === "local" && selector !== "local") {
1000
+ throw new Error("invalid local Runtime target selector");
1001
+ }
1002
+ if (record.binding === "direct" && (selector !== daemonId || typeof record.endpoint !== "string")) {
1003
+ throw new Error("invalid Direct Runtime target view");
1004
+ }
1005
+ return {
1006
+ selector,
1007
+ daemonId,
1008
+ displayName: record.displayName,
1009
+ binding: record.binding,
1010
+ ...(record.endpoint === undefined ? {} : { endpoint: record.endpoint }),
1011
+ ...(record.identityKeyFingerprint === undefined
1012
+ ? {}
1013
+ : { identityKeyFingerprint: record.identityKeyFingerprint }),
1014
+ ...(record.createdAt === undefined ? {} : { createdAt: record.createdAt }),
1015
+ };
1016
+ }
1017
+ function validRuntimeSelector(value) {
1018
+ if (typeof value !== "string" ||
1019
+ value.length === 0 ||
1020
+ value.length > 256 ||
1021
+ !OPAQUE_ID_PATTERN.test(value)) {
1022
+ throw new Error("invalid Runtime target selector");
1023
+ }
1024
+ return value;
1025
+ }
1026
+ function messageOf(error) {
1027
+ return error instanceof Error ? error.message : String(error);
1028
+ }