@hediet/linkrpc-mcp 0.0.1

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.
@@ -0,0 +1,2530 @@
1
+ import { createRequire } from "node:module";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ErrorCode, InMemoryManagedIdentity, JsonRpcChannel, RpcError, SigningSender, capabilityFreshAt, generateTsInterface, permissionMatchesTarget, traceMessageTransport } from "@hediet/linkrpc";
5
+ import { z } from "zod";
6
+ import { LINKRPC_ENDPOINT_VAR, LINKRPC_TOKEN_VAR, WebSocketTransport, connectNdjson, createManagedPrincipal, createSelfManagedPrincipal, createSelfManagedPrincipalFromFile, formatEndpointUri, loadOrCreateIdentity, openWebSocket, parseEndpointUri, runInitializeHandshake } from "@hediet/linkrpc/node";
7
+ import { Hub, HubConnectionAcceptor, anonymousHandler, createHubServiceInterfaces } from "@hediet/linkrpc-hub/hub/server/client";
8
+ import { SocketServer } from "@hediet/linkrpc-hub/hub/server/node";
9
+ import { randomBytes } from "node:crypto";
10
+ import * as fs from "node:fs";
11
+ import { readFileSync } from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { spawnCommand, spawnCommand as spawnCommand$1 } from "@hediet/linkrpc-hub/spawn";
15
+ import { tapTransport } from "@hediet/linkrpc-hub/hub/server/transit";
16
+ import * as net from "node:net";
17
+ import "@hediet/linkrpc-hub/config";
18
+ import { fetchSchema, hubAccessInterface, walkHubDetailed } from "@hediet/linkrpc/hub/common";
19
+ import { Scope, getQuickJS } from "quickjs-emscripten";
20
+ import { fileURLToPath } from "node:url";
21
+ import { createHash } from "crypto";
22
+ import { ContentBlockSchema } from "@modelcontextprotocol/sdk/types.js";
23
+ //#region ../../packages-private/linkrpc-client/src/localHub.ts
24
+ /** Default serviceId namespace the local-hub child may claim (and sees via `hubGrantedServiceId::get`). */
25
+ const DEFAULT_LOCAL_NAMESPACE = "local";
26
+ /** Subfolder (under the linkrpc data dir) holding provisioned identity slots. */
27
+ const PROVISION_SUBDIR = "provisioned-identities";
28
+ /** Provisioned slots untouched for longer than this are swept on next run. */
29
+ const PROVISION_MAX_AGE_MS = 2592e6;
30
+ /**
31
+ * Start an in-process hub on a private socket, spawn `command` as a participant
32
+ * (handing it the socket + token via `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN`), and
33
+ * resolve once the child has registered a service under {@link LOCAL_NAMESPACE}.
34
+ *
35
+ * The hub is single-tenant and local: no claim policy (every well-formed claim
36
+ * is allowed) and no provenance. Identity is either ephemeral (fresh per run)
37
+ * or, when `provisionSlot` is set, a persisted managed identity so the child's
38
+ * HPKE wrap/unwrap keys survive across runs.
39
+ */
40
+ async function startLocalHub(opts) {
41
+ const resolveIdentity = _makeIdentityResolver(opts.provisionSlot);
42
+ const grantedNs = DEFAULT_LOCAL_NAMESPACE;
43
+ const hub = new Hub();
44
+ createHubServiceInterfaces(hub);
45
+ const socketPath = SocketServer.allocSocketPath();
46
+ const token = randomBytes(16).toString("hex");
47
+ const socketServer = await SocketServer.start({ endpoint: socketPath });
48
+ const acceptor = new HubConnectionAcceptor({
49
+ server: socketServer,
50
+ hub,
51
+ handlers: [anonymousHandler({
52
+ grantedServiceIdNamespace: grantedNs,
53
+ ...resolveIdentity ? { resolveIdentity } : {}
54
+ })]
55
+ });
56
+ const child = spawnCommand(opts.command, {
57
+ stdio: [
58
+ "inherit",
59
+ "inherit",
60
+ "inherit"
61
+ ],
62
+ env: {
63
+ ...process.env,
64
+ ...opts.env,
65
+ LINKRPC_ENDPOINT: socketPath,
66
+ LINKRPC_TOKEN: token
67
+ },
68
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
69
+ });
70
+ let childExited = false;
71
+ child.once("exit", () => {
72
+ childExited = true;
73
+ });
74
+ const dispose = () => {
75
+ if (!child.killed) child.kill();
76
+ acceptor.dispose();
77
+ socketServer.dispose();
78
+ if (process.platform !== "win32") try {
79
+ fs.unlinkSync(socketPath);
80
+ } catch {}
81
+ };
82
+ try {
83
+ await _waitForClaim(hub, grantedNs, () => childExited, opts.readyTimeoutMs ?? 3e4);
84
+ } catch (e) {
85
+ dispose();
86
+ throw e;
87
+ }
88
+ return {
89
+ socketPath,
90
+ token,
91
+ dispose
92
+ };
93
+ }
94
+ /**
95
+ * Build the hub's `resolveIdentity`, or `undefined` when no identity should be
96
+ * provided. Without `provisionSlot` the hub serves no identity at all — a child
97
+ * that needs `identity::*` will fail. With it, a single persisted managed
98
+ * identity is shared by all connections (stale slots swept first), so the
99
+ * child's HPKE wrap/unwrap keys survive across runs.
100
+ */
101
+ function _makeIdentityResolver(provisionSlot) {
102
+ if (provisionSlot === void 0) return;
103
+ const dir = _provisionDir();
104
+ _sweepProvisionedIdentities(dir);
105
+ let shared;
106
+ return () => {
107
+ if (!shared) shared = (async () => {
108
+ const persisted = await loadOrCreateIdentity({
109
+ id: provisionSlot,
110
+ storeDir: dir
111
+ });
112
+ return new InMemoryManagedIdentity(persisted.keypair, persisted.wrapKeypair);
113
+ })();
114
+ return shared;
115
+ };
116
+ }
117
+ /** Dedicated provisioned-identity folder (sibling of linkrpc's user identities). */
118
+ function _provisionDir() {
119
+ const home = os.homedir();
120
+ let base;
121
+ if (process.platform === "win32") base = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
122
+ else if (process.platform === "darwin") base = path.join(home, "Library", "Application Support");
123
+ else base = process.env.XDG_CONFIG_HOME ?? path.join(home, ".config");
124
+ return path.join(base, "linkrpc", PROVISION_SUBDIR);
125
+ }
126
+ /** Delete provisioned identity files whose mtime is older than the max age. */
127
+ function _sweepProvisionedIdentities(dir) {
128
+ let entries;
129
+ try {
130
+ entries = fs.readdirSync(dir);
131
+ } catch {
132
+ return;
133
+ }
134
+ const cutoff = Date.now() - PROVISION_MAX_AGE_MS;
135
+ for (const name of entries) {
136
+ if (!name.endsWith(".json")) continue;
137
+ const file = path.join(dir, name);
138
+ try {
139
+ if (fs.statSync(file).mtimeMs < cutoff) fs.unlinkSync(file);
140
+ } catch {}
141
+ }
142
+ }
143
+ /** Resolve once the child claims `prefix` (or a sub-prefix), else reject. */
144
+ function _waitForClaim(hub, prefix, childExited, timeoutMs) {
145
+ const deadline = Date.now() + timeoutMs;
146
+ return new Promise((resolve, reject) => {
147
+ const check = () => {
148
+ if (hub.claimedPrefixes().some((p) => p === prefix || p.startsWith(`${prefix}/`))) {
149
+ resolve();
150
+ return;
151
+ }
152
+ if (childExited()) {
153
+ reject(/* @__PURE__ */ new Error("connect: command exited before registering a service"));
154
+ return;
155
+ }
156
+ if (Date.now() >= deadline) {
157
+ reject(/* @__PURE__ */ new Error(`connect: timed out waiting for command to register a service under '${prefix}'`));
158
+ return;
159
+ }
160
+ setTimeout(check, 50);
161
+ };
162
+ check();
163
+ });
164
+ }
165
+ //#endregion
166
+ //#region ../../packages-private/linkrpc-client/src/connect.ts
167
+ async function connect(endpoint, log) {
168
+ switch (endpoint.kind) {
169
+ case "cmd-stdio": return _connectCmdStdio(endpoint.command, endpoint.env, endpoint.cwd, log);
170
+ case "cmd-env": return _connectCmdEnv(endpoint.command, endpoint.provisionSlot, endpoint.env, endpoint.cwd, log);
171
+ case "ws": return _connectWs(endpoint, log);
172
+ case "ws-no-init": return _connectWs(endpoint, log);
173
+ case "socket": return _connectSocket(endpoint.path, endpoint.token, log);
174
+ }
175
+ }
176
+ /**
177
+ * Spawn a child from a command spec. `{ command }` is run through the OS shell
178
+ * (so quoting / splitting follows the shell's rules); `{ argv }` is run
179
+ * directly (no shell), except on Windows where `.cmd` shims need one.
180
+ */
181
+ async function _connectCmdStdio(command, env, cwd, log) {
182
+ const child = spawnCommand$1(command, {
183
+ stdio: [
184
+ "pipe",
185
+ "pipe",
186
+ "inherit"
187
+ ],
188
+ ...env !== void 0 ? { env: {
189
+ ...process.env,
190
+ ...env
191
+ } } : {},
192
+ ...cwd !== void 0 ? { cwd } : {}
193
+ });
194
+ if (!child.stdin || !child.stdout) throw new Error("connect: child process exposes no stdio");
195
+ const { transport } = await connectNdjson({
196
+ input: child.stdout,
197
+ output: child.stdin,
198
+ onClose: () => {
199
+ if (!child.killed) child.kill();
200
+ },
201
+ trace: log?.trace
202
+ });
203
+ return _makeCliConnection(transport, () => {
204
+ if (!child.killed) child.kill();
205
+ }, log);
206
+ }
207
+ /**
208
+ * Start a private in-process hub, spawn the child as a participant, then
209
+ * connect to the hub's socket. The child registers its services against the
210
+ * hub exactly as it would against a remote one; we tear the hub + child down
211
+ * when the connection closes.
212
+ */
213
+ async function _connectCmdEnv(command, provisionSlot, env, cwd, log) {
214
+ return connectViaLocalHub({
215
+ command,
216
+ provisionSlot,
217
+ env,
218
+ cwd,
219
+ log
220
+ });
221
+ }
222
+ /**
223
+ * Spawn a child under an in-process local hub (`startLocalHub`) and connect
224
+ * to that hub over a socket. Backs the standard `cmd-env` endpoint path; the
225
+ * hub + child are torn down when the connection closes.
226
+ */
227
+ async function connectViaLocalHub(opts) {
228
+ const hub = await startLocalHub({
229
+ command: opts.command,
230
+ provisionSlot: opts.provisionSlot,
231
+ env: opts.env,
232
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
233
+ });
234
+ try {
235
+ const conn = await _connectSocket(hub.socketPath, hub.token, opts.log);
236
+ return {
237
+ ...conn,
238
+ close: () => {
239
+ conn.close();
240
+ hub.dispose();
241
+ }
242
+ };
243
+ } catch (e) {
244
+ hub.dispose();
245
+ throw e;
246
+ }
247
+ }
248
+ function _connectWs(endpoint, log) {
249
+ return openWebSocket(endpoint.url).then(async (ws) => {
250
+ const closeWs = () => {
251
+ try {
252
+ ws.close();
253
+ } catch {}
254
+ };
255
+ const baseTransport = new WebSocketTransport(ws, closeWs);
256
+ const transport = log?.trace === void 0 ? baseTransport : traceMessageTransport(baseTransport, log.trace);
257
+ if (endpoint.kind === "ws") try {
258
+ await runInitializeHandshake(transport, {
259
+ kind: "client",
260
+ token: endpoint.token ?? ""
261
+ });
262
+ } catch (err) {
263
+ transport.dispose();
264
+ closeWs();
265
+ throw err;
266
+ }
267
+ return _makeCliConnection(transport, closeWs, log);
268
+ });
269
+ }
270
+ async function _connectSocket(socketPath, token, log) {
271
+ const socket = net.createConnection(socketPath);
272
+ await new Promise((resolve, reject) => {
273
+ const onConnect = () => {
274
+ socket.removeListener("error", onError);
275
+ resolve();
276
+ };
277
+ const onError = (error) => {
278
+ socket.removeListener("connect", onConnect);
279
+ socket.destroy();
280
+ reject(error);
281
+ };
282
+ socket.once("connect", onConnect);
283
+ socket.once("error", onError);
284
+ });
285
+ socket.on("error", () => socket.destroy());
286
+ const { transport } = await connectNdjson({
287
+ input: socket,
288
+ output: socket,
289
+ onClose: () => socket.destroy(),
290
+ initialize: {
291
+ kind: "client",
292
+ token: token ?? ""
293
+ },
294
+ trace: log?.trace
295
+ });
296
+ return _makeCliConnection(transport, () => socket.destroy(), log);
297
+ }
298
+ /**
299
+ * In-memory connection — for tests. The caller hands us a transport already
300
+ * wired to a server-side channel (typically via `TransportPair`).
301
+ */
302
+ function connectViaTransport(transport) {
303
+ return _makeCliConnection(transport, () => {});
304
+ }
305
+ function _makeCliConnection(transport, onClose, log) {
306
+ const tapped = log?.log === void 0 ? transport : tapTransport(transport, {
307
+ log: log.log,
308
+ localLabel: "cli",
309
+ remoteLabel: log.remoteLabel ?? "peer",
310
+ ...log.maxPayload !== void 0 ? { maxPayload: log.maxPayload } : {}
311
+ });
312
+ const signing = {};
313
+ const wrapped = SigningSender.wrapChannel(JsonRpcChannel.create(tapped), signing);
314
+ const channel = wrapped.sender;
315
+ return {
316
+ channel,
317
+ rpcChannel: wrapped,
318
+ signing,
319
+ setRequestHandler: (handler) => wrapped.setRequestHandler(handler),
320
+ close: () => {
321
+ channel.close();
322
+ onClose();
323
+ }
324
+ };
325
+ }
326
+ //#endregion
327
+ //#region ../../packages-private/linkrpc-client/src/principal.ts
328
+ /**
329
+ * Identity slot the managed-with-fallback default falls back to when the peer
330
+ * does not offer a managed identity overlay (e.g. a plain stdio server). Maps
331
+ * to the same on-disk slot `logout` clears.
332
+ */
333
+ const MANAGED_FALLBACK_USER_ID = "hubrpc-cli";
334
+ /**
335
+ * Resolve a {@link PrincipalSpec} into a concrete {@link Principal}, given the
336
+ * (signed) sender used to bootstrap a managed identity. Cheap/idempotent, so
337
+ * it can be re-derived on each reconnect. Also reports the {@link PrincipalSource}
338
+ * that was actually used, including whether `managed` fell back to a local key.
339
+ */
340
+ async function resolvePrincipal(spec, sender) {
341
+ switch (spec.kind) {
342
+ case "managed": try {
343
+ return {
344
+ principal: await createManagedPrincipal(sender),
345
+ source: { kind: "managed" }
346
+ };
347
+ } catch {
348
+ return {
349
+ principal: await createSelfManagedPrincipal(MANAGED_FALLBACK_USER_ID),
350
+ source: {
351
+ kind: "managed-fallback",
352
+ userId: MANAGED_FALLBACK_USER_ID
353
+ }
354
+ };
355
+ }
356
+ case "user": return {
357
+ principal: await createSelfManagedPrincipal(spec.id),
358
+ source: {
359
+ kind: "user",
360
+ id: spec.id
361
+ }
362
+ };
363
+ case "file": return {
364
+ principal: await createSelfManagedPrincipalFromFile(spec.path),
365
+ source: {
366
+ kind: "file",
367
+ path: spec.path
368
+ }
369
+ };
370
+ }
371
+ }
372
+ //#endregion
373
+ //#region ../../packages-private/linkrpc-client/src/hubSigning.ts
374
+ /** How long before expiry to refresh a capability (2 seconds = transit + skew). */
375
+ const CAP_FRESHNESS_MARGIN_MS = 2e3;
376
+ /**
377
+ * Install signing on `signing` for any endpoint. Resolves the
378
+ * {@link PrincipalSpec} into a concrete {@link Principal} (managed-with-
379
+ * fallback, a user slot, or a file-backed keypair) and points the channel's
380
+ * `SigningSender` at it so every outbound call is signed.
381
+ *
382
+ * When `negotiateHubCaps` is set (hub endpoints), it also ensures a persistent
383
+ * `hubAccess` capability is cached on the principal's {@link CapBag} — requesting
384
+ * one with a single signed round-trip on first run / cache miss. For hub
385
+ * endpoints, it also installs a sign-time capability provider.
386
+ *
387
+ * By default (`autoNegotiatePerCall !== false`) that provider negotiates
388
+ * per-call authority lazily using `callIntent` (method + params + nonce +
389
+ * signedAtMs + optional interfaceHash), so one-shot grants can be pinned to the
390
+ * exact call bytes being signed. Set `autoNegotiatePerCall: false` to disable
391
+ * that: the provider then only presents caps already in the bag, and the caller
392
+ * is expected to request access explicitly via
393
+ * {@link SigningSession.requestAccess}.
394
+ */
395
+ async function setupSigning(channel, signing, principalSpec, opts) {
396
+ const { principal, source: principalSource } = await resolvePrincipal(principalSpec, channel);
397
+ signing.principal = principal;
398
+ signing.oneShotCaps = void 0;
399
+ signing.capProvider = void 0;
400
+ const hubAccessMethod = `${hubAccessInterface.info.id}::requestAccess`;
401
+ const consumerPrincipalId = principal.id;
402
+ if (opts.negotiateHubCaps) {
403
+ let inAccessNegotiation = false;
404
+ const provider = async ({ method, params, nonce, signedAtMs, interfaceHash }) => {
405
+ let presentCaps = principal.capBag.capabilities;
406
+ presentCaps = presentCaps.filter((c) => capabilityFreshAt(c, signedAtMs, CAP_FRESHNESS_MARGIN_MS));
407
+ const present = presentCaps.length > 0 ? { capabilities: presentCaps } : {};
408
+ if (inAccessNegotiation) return {};
409
+ const call = _wireMethodToCall(method);
410
+ if (!call) return present;
411
+ if (call.serviceId === "" || call.interfaceId === hubAccessInterface.info.id) return present;
412
+ if (_capBagCovers(presentCaps, call)) return present;
413
+ if (opts.autoNegotiatePerCall === false) return present;
414
+ inAccessNegotiation = true;
415
+ try {
416
+ const granted = await _requestAccessForCall(channel, hubAccessMethod, consumerPrincipalId, {
417
+ call,
418
+ method,
419
+ params,
420
+ nonce,
421
+ signedAtMs,
422
+ interfaceHash
423
+ });
424
+ if (granted.length === 0) return present;
425
+ const oneShot = granted.filter(_isOneShotCap);
426
+ const persistent = granted.filter((c) => !_isOneShotCap(c));
427
+ if (persistent.length > 0) await principal.capBag.add(...persistent);
428
+ const durable = principal.capBag.capabilities.filter((c) => capabilityFreshAt(c, signedAtMs, CAP_FRESHNESS_MARGIN_MS));
429
+ if (oneShot.length > 0) return { capabilities: [...durable, ...oneShot] };
430
+ return durable.length > 0 ? { capabilities: durable } : {};
431
+ } catch (err) {
432
+ process.stderr.write(`linkrpc: capability negotiation skipped (${err.message})\n`);
433
+ return present;
434
+ } finally {
435
+ inAccessNegotiation = false;
436
+ }
437
+ };
438
+ signing.capProvider = provider;
439
+ }
440
+ return {
441
+ principal,
442
+ principalSource,
443
+ hubAccessMethod,
444
+ listGrants: () => principal.capBag.capabilities,
445
+ requestAccess: (req) => _sessionRequestAccess(channel, hubAccessMethod, principal, req)
446
+ };
447
+ }
448
+ /** Parse a wire method into a {@link CallTarget}, or `undefined` for form-1/2. */
449
+ function _wireMethodToCall(wireMethod) {
450
+ const parts = wireMethod.split("::");
451
+ if (parts.length !== 3) return void 0;
452
+ const [serviceId, interfaceId, member] = parts;
453
+ return {
454
+ serviceId,
455
+ interfaceId,
456
+ member
457
+ };
458
+ }
459
+ /** A cap is one-shot when any permission is pinned to a single call via `callBind`. */
460
+ function _isOneShotCap(sc) {
461
+ return sc.permissions.some((p) => p.callBind !== void 0);
462
+ }
463
+ /** True when a durable (non-one-shot) cap in the bag authorises `target`. */
464
+ function _capBagCovers(caps, target) {
465
+ return caps.some((sc) => !_isOneShotCap(sc) && sc.permissions.some((p) => permissionMatchesTarget(target, p)));
466
+ }
467
+ async function _requestAccessForCall(channel, hubAccessMethod, consumerPrincipalId, req) {
468
+ const result = await _sendRequestAccess(channel, hubAccessMethod, {
469
+ consumer: {
470
+ name: "linkrpc-cli",
471
+ principal: consumerPrincipalId,
472
+ purpose: `Invoke ${req.method}.`
473
+ },
474
+ permissions: [{
475
+ target: {
476
+ serviceId: { exact: req.call.serviceId },
477
+ interfaceId: { exact: req.call.interfaceId },
478
+ members: [{ exact: req.call.member }]
479
+ },
480
+ canInvoke: true,
481
+ callIntent: {
482
+ method: req.method,
483
+ params: req.params,
484
+ nonce: req.nonce,
485
+ signedAtMs: req.signedAtMs,
486
+ ...req.interfaceHash !== void 0 ? { interfaceHash: req.interfaceHash } : {},
487
+ suggestion: "once"
488
+ }
489
+ }],
490
+ duration: "once"
491
+ });
492
+ if (result.status !== "granted") return [];
493
+ return result.capabilities ?? [];
494
+ }
495
+ async function _sendRequestAccess(channel, hubAccessMethod, params) {
496
+ return await _awaitWithApprovalNotice(channel.sendRequest(hubAccessMethod, params));
497
+ }
498
+ /** Delay before we tell the user an access request is parked awaiting approval. */
499
+ const APPROVAL_NOTICE_DELAY_MS = 750;
500
+ /**
501
+ * Await a `hubAccess::requestAccess` round-trip, printing a one-line hint to
502
+ * stderr if it doesn't resolve quickly. Access requests park at the hub until a
503
+ * human approver (admin) decides them, so without this notice the CLI looks
504
+ * hung — it blocks with no output until approval. Fast requests (auto-approved
505
+ * / open hub) stay silent because the notice only fires after
506
+ * {@link APPROVAL_NOTICE_DELAY_MS}.
507
+ */
508
+ async function _awaitWithApprovalNotice(pending) {
509
+ const timer = setTimeout(() => {
510
+ process.stderr.write("linkrpc: access request sent — waiting for the hub admin to approve it...\n");
511
+ }, APPROVAL_NOTICE_DELAY_MS);
512
+ timer.unref?.();
513
+ try {
514
+ return await pending;
515
+ } finally {
516
+ clearTimeout(timer);
517
+ }
518
+ }
519
+ /**
520
+ * Backs {@link SigningSession.requestAccess}. Sends a batched
521
+ * `hubAccess::requestAccess`, then adds any durable (non-one-shot) caps the hub
522
+ * minted to the principal's cap bag so later calls present them automatically.
523
+ */
524
+ async function _sessionRequestAccess(channel, hubAccessMethod, principal, req) {
525
+ const result = await _sendRequestAccess(channel, hubAccessMethod, {
526
+ consumer: {
527
+ ...req.consumer,
528
+ principal: principal.id
529
+ },
530
+ permissions: req.permissions,
531
+ ...req.duration !== void 0 ? { duration: req.duration } : {}
532
+ });
533
+ if (result.status === "granted") {
534
+ const capabilities = result.capabilities ?? [];
535
+ const durable = capabilities.filter((c) => !_isOneShotCap(c));
536
+ if (durable.length > 0) await principal.capBag.add(...durable);
537
+ return {
538
+ status: "granted",
539
+ capabilities,
540
+ addedDurable: durable.length
541
+ };
542
+ }
543
+ return {
544
+ status: result.status,
545
+ reason: result.reason
546
+ };
547
+ }
548
+ //#endregion
549
+ //#region src/connectionPool.ts
550
+ /**
551
+ * Maintains live hub connections keyed by the canonical endpoint URI (path/url
552
+ * + token). Connections are created lazily on first use and reused for every
553
+ * subsequent call against the same endpoint. Setting up the hub signing session
554
+ * (which may surface a consent modal the first time) happens once per endpoint
555
+ * and the resulting session is cached on the pool entry.
556
+ */
557
+ var ConnectionPool = class ConnectionPool {
558
+ _entries = /* @__PURE__ */ new Map();
559
+ _defaultEndpoint;
560
+ _defaultTransport;
561
+ /** Pool key for the in-process default connection (see {@link ConnectionPoolOptions.defaultTransport}). */
562
+ static _DEFAULT_INPROC_KEY = "<default-inproc>";
563
+ constructor(options = {}) {
564
+ this._defaultEndpoint = options.defaultEndpoint;
565
+ this._defaultTransport = options.defaultTransport;
566
+ }
567
+ /**
568
+ * Resolve a pooled connection for `endpointUri` (a strict endpoint URI such
569
+ * as `unix:/path?token=…`, `npipe://./pipe/…?token=…`, or
570
+ * `wss://host?token=…`). When omitted, an in-process default transport (if
571
+ * configured) is used; otherwise it falls back to the configured default
572
+ * endpoint, then to the `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN` env vars. A token
573
+ * absent from the URI is filled in from `LINKRPC_TOKEN` when present.
574
+ */
575
+ async resolve(endpointUri) {
576
+ if (endpointUri === void 0 && this._defaultTransport) return this._resolveCached(ConnectionPool._DEFAULT_INPROC_KEY, (key) => this._openInProc(this._defaultTransport(), key));
577
+ const spec = this._resolveSpec(endpointUri);
578
+ const key = formatEndpointUri(spec, { revealToken: true });
579
+ return this._resolveCached(key, (k) => this._open(spec, k));
580
+ }
581
+ /**
582
+ * Shared cache-or-open: returns the in-flight/cached entry for `key`, or
583
+ * starts `open(key)` and caches the promise so concurrent callers share the
584
+ * same signing setup. On failure the entry is evicted so the next call
585
+ * retries cleanly.
586
+ */
587
+ _resolveCached(key, open) {
588
+ const existing = this._entries.get(key);
589
+ if (existing) return existing;
590
+ const pending = open(key);
591
+ this._entries.set(key, pending);
592
+ pending.catch(() => this._entries.delete(key));
593
+ return pending;
594
+ }
595
+ _resolveSpec(endpointUri) {
596
+ if (endpointUri !== void 0) return _withEnvToken(parseEndpointUri(endpointUri));
597
+ return this._defaultEndpoint ?? _resolveFromEnv();
598
+ }
599
+ async _open(spec, key) {
600
+ const display = formatEndpointUri(spec);
601
+ const cli = await connect(spec);
602
+ return this._finishOpen(cli, display, key);
603
+ }
604
+ async _openInProc(def, key) {
605
+ const cli = connectViaTransport(def.transport);
606
+ return this._finishOpen(cli, def.label ?? "inproc", key, def.dispose);
607
+ }
608
+ /**
609
+ * Wrap an already-open {@link CliConnection} in a managed signing session
610
+ * and build the {@link PooledConnection}. Shared by the dialed-socket and
611
+ * in-process default paths — both sign as a managed principal and bootstrap
612
+ * the `hubAccess` cap, the only difference being how the transport was
613
+ * obtained. `extraDispose` runs on disposal after the connection is closed.
614
+ */
615
+ async _finishOpen(cli, display, key, extraDispose) {
616
+ const traceListeners = /* @__PURE__ */ new Set();
617
+ const trace = (line) => {
618
+ process.stderr.write(`${line}\n`);
619
+ for (const l of traceListeners) try {
620
+ l(line);
621
+ } catch {}
622
+ };
623
+ _installTransportTrace(cli.channel, display, trace);
624
+ try {
625
+ trace(`linkrpc-mcp[${display}] requesting hub signing session (may prompt for reflection-cap consent on first use)`);
626
+ const session = await setupSigning(cli.channel, cli.signing, { kind: "managed" }, {
627
+ negotiateHubCaps: true,
628
+ autoNegotiatePerCall: false
629
+ });
630
+ return {
631
+ channel: cli.channel,
632
+ endpoint: display,
633
+ key,
634
+ session,
635
+ lastResultVal: void 0,
636
+ addTraceListener: (listener) => {
637
+ traceListeners.add(listener);
638
+ return () => traceListeners.delete(listener);
639
+ },
640
+ trace,
641
+ dispose: () => {
642
+ cli.close();
643
+ extraDispose?.();
644
+ this._entries.delete(key);
645
+ }
646
+ };
647
+ } catch (e) {
648
+ cli.close();
649
+ extraDispose?.();
650
+ throw e;
651
+ }
652
+ }
653
+ dispose() {
654
+ for (const pending of this._entries.values()) pending.then((e) => e.dispose(), () => {});
655
+ this._entries.clear();
656
+ }
657
+ };
658
+ /**
659
+ * Fill a missing socket/ws token from `LINKRPC_TOKEN` when the URI itself didn't
660
+ * carry one. Command endpoints (`cmd:` / `cmd-stdio:`) carry no token.
661
+ */
662
+ function _withEnvToken(spec) {
663
+ if ((spec.kind === "socket" || spec.kind === "ws") && spec.token === void 0) {
664
+ const token = process.env[LINKRPC_TOKEN_VAR];
665
+ if (token) return {
666
+ ...spec,
667
+ token
668
+ };
669
+ }
670
+ return spec;
671
+ }
672
+ function _resolveFromEnv() {
673
+ const endpoint = process.env[LINKRPC_ENDPOINT_VAR];
674
+ if (!endpoint) throw new Error(`No connection supplied and ${LINKRPC_ENDPOINT_VAR} is not set. Pass a 'connection' endpoint URI (e.g. unix:/path?token=… or wss://host?token=…) to the tool, or run the MCP server inside a VS Code window with the team-tools hub active.`);
675
+ return _withEnvToken(parseEndpointUri(endpoint));
676
+ }
677
+ /**
678
+ * Wrap `channel.sendRequest` / `channel.sendNotification` so every
679
+ * outbound JSON-RPC envelope and its outcome is emitted as a trace
680
+ * line. Catches everything the MCP server drives (tool-issued `call` /
681
+ * `notify`, `explore`, and the hub's `hubAccess::requestAccess`
682
+ * permission round-trips).
683
+ *
684
+ * IMPORTANT: the wrappers forward the third `opts` argument verbatim. It
685
+ * carries the per-call {@link SigningCallCtx} (e.g. `signerOverride: null` for
686
+ * the unsigned `identity::*` bootstrap round-trips). Dropping it would make a
687
+ * managed identity's `identity::sign` calls get signed — which recurses
688
+ * (signing a call needs another `identity::sign`), blowing the heap.
689
+ */
690
+ function _installTransportTrace(channel, endpoint, trace) {
691
+ const send = channel.sendRequest.bind(channel);
692
+ const notify = channel.sendNotification.bind(channel);
693
+ let seq = 0;
694
+ channel.sendRequest = async (method, params, opts) => {
695
+ const id = ++seq;
696
+ trace(`linkrpc-mcp[${endpoint}] → #${id} request ${method} ${_fmt(params)}`);
697
+ try {
698
+ const result = await send(method, params, opts);
699
+ trace(`linkrpc-mcp[${endpoint}] ← #${id} result ${_fmt(result)}`);
700
+ return result;
701
+ } catch (e) {
702
+ trace(`linkrpc-mcp[${endpoint}] ← #${id} error ${e.message}`);
703
+ throw e;
704
+ }
705
+ };
706
+ channel.sendNotification = async (method, params, opts) => {
707
+ trace(`linkrpc-mcp[${endpoint}] → #${++seq} notify ${method} ${_fmt(params)}`);
708
+ await notify(method, params, opts);
709
+ };
710
+ }
711
+ const _MAX_TRACE_PAYLOAD = 2e3;
712
+ function _fmt(v) {
713
+ if (v === void 0) return "(no params)";
714
+ let s;
715
+ try {
716
+ s = JSON.stringify(v);
717
+ } catch {
718
+ s = String(v);
719
+ }
720
+ if (s === void 0) s = "undefined";
721
+ return s.length > _MAX_TRACE_PAYLOAD ? `${s.slice(0, _MAX_TRACE_PAYLOAD)}…(+${s.length - _MAX_TRACE_PAYLOAD} chars)` : s;
722
+ }
723
+ //#endregion
724
+ //#region src/senderProvider.ts
725
+ /**
726
+ * {@link IConnectionPool} backed by a {@link HubSenderProvider}: caches one
727
+ * {@link HubSigningSender} per distinct `connection` argument and adapts it to
728
+ * the {@link PooledConnection} shape the MCP tools consume. Holds the
729
+ * MCP-layer-only `lastResultVal` and trace fan-out so the sender stays pure.
730
+ */
731
+ var ProviderPool = class {
732
+ _provider;
733
+ _session;
734
+ _entries = /* @__PURE__ */ new Map();
735
+ constructor(_provider, _session) {
736
+ this._provider = _provider;
737
+ this._session = _session;
738
+ }
739
+ resolve(endpointUri) {
740
+ const key = endpointUri ?? "<default>";
741
+ const existing = this._entries.get(key);
742
+ if (existing) return existing;
743
+ const pending = this._open(endpointUri, key);
744
+ this._entries.set(key, pending);
745
+ pending.catch(() => this._entries.delete(key));
746
+ return pending;
747
+ }
748
+ async _open(endpointUri, key) {
749
+ const sender = await this._provider(this._session, endpointUri);
750
+ const traceListeners = /* @__PURE__ */ new Set();
751
+ return {
752
+ channel: sender,
753
+ endpoint: sender.identity.principal,
754
+ key,
755
+ session: sender,
756
+ lastResultVal: void 0,
757
+ addTraceListener: (listener) => {
758
+ traceListeners.add(listener);
759
+ return () => traceListeners.delete(listener);
760
+ },
761
+ trace: (line) => {
762
+ for (const l of traceListeners) try {
763
+ l(line);
764
+ } catch {}
765
+ },
766
+ dispose: () => {
767
+ sender.close();
768
+ this._entries.delete(key);
769
+ }
770
+ };
771
+ }
772
+ dispose() {
773
+ for (const pending of this._entries.values()) pending.then((e) => e.dispose(), () => {});
774
+ this._entries.clear();
775
+ }
776
+ };
777
+ //#endregion
778
+ //#region src/grants.ts
779
+ /** Render a single `serviceId` / `interfaceId` / member matcher as a string. */
780
+ function _fmtPattern(p) {
781
+ if ("exact" in p) return p.exact;
782
+ return p.prefix === "" ? "*" : `${p.prefix}*`;
783
+ }
784
+ /**
785
+ * Summarise the durable capabilities a connection currently holds into a
786
+ * compact, JSON-friendly shape the model can read to understand what access it
787
+ * already has (and therefore what it still needs to request).
788
+ */
789
+ function summarizeGrants(caps) {
790
+ const grants = caps.map((c) => {
791
+ const oneShot = c.permissions.some((p) => p.callBind !== void 0);
792
+ return {
793
+ issuer: c.issuer,
794
+ audience: c.audience,
795
+ ...c.expiresAtMs !== void 0 ? { expiresAtMs: c.expiresAtMs } : {},
796
+ oneShot,
797
+ permissions: c.permissions.map((p) => ({
798
+ serviceId: _fmtPattern(p.target.serviceId),
799
+ interfaceId: _fmtPattern(p.target.interfaceId),
800
+ members: p.target.members.map(_fmtPattern),
801
+ canInvoke: p.canInvoke ?? false,
802
+ canDelegate: p.canDelegate ?? false
803
+ }))
804
+ };
805
+ });
806
+ return {
807
+ count: grants.length,
808
+ grants
809
+ };
810
+ }
811
+ //#endregion
812
+ //#region src/connectionDts.ts
813
+ /**
814
+ * Documentation that the MCP server exposes as a resource. The text is the
815
+ * single source of truth for what `runLinkRpcScript` sees inside the QuickJS
816
+ * sandbox — it lives as the real declaration file `connection.d.ts` and is
817
+ * embedded here verbatim. Keep `connection.d.ts` in sync with `sandbox.ts`
818
+ * and `explore.ts`.
819
+ */
820
+ const CONNECTION_DTS = "// ---- runLinkRpcScript sandbox API ----------------------------------------\n//\n// The MCP tool `runLinkRpcScript` takes `code` (a JS function expression) and\n// invokes it inside a QuickJS sandbox with the signature below. Anything the\n// function returns (sync or async) becomes the tool's result, JSON-encoded.\n//\n// ({ con, lastResultVal, mcp }) => any | Promise<any>\n//\n// Example:\n//\n// ({ con }) => con.call(\"vscode\", \"vscode.window\",\n// \"showInformationMessage\",\n// { message: \"hello\" })\n//\n// The sandbox is isolated: no `require`, no `process`, no host globals\n// other than the ones declared here. Memory is capped (~32 MB) and execution\n// is interrupted after ~5 s.\n\ninterface RunSvcContext {\n /** The live connection wired to the requested hub. */\n readonly con: SvcConnection;\n /**\n * The value returned by the previous `runLinkRpcScript` call on the same hub\n * connection. JSON round-tripped — non-serialisable members are dropped.\n * `undefined` on the first call.\n */\n readonly lastResultVal: unknown;\n /**\n * Optional presentation helpers. Ordinary return values are automatically\n * converted to native MCP image/audio/resource blocks when possible.\n */\n readonly mcp: McpPresentation;\n}\n\ninterface McpAnnotations {\n readonly audience?: ReadonlyArray<\"user\" | \"assistant\">;\n readonly priority?: number;\n readonly lastModified?: string;\n}\n\ntype McpMeta = Record<string, unknown>;\n\ntype McpContentBlock =\n | {\n readonly type: \"text\";\n readonly text: string;\n readonly annotations?: McpAnnotations;\n readonly _meta?: McpMeta;\n }\n | {\n readonly type: \"image\";\n readonly data: string;\n readonly mimeType: string;\n readonly annotations?: McpAnnotations;\n readonly _meta?: McpMeta;\n }\n | {\n readonly type: \"audio\";\n readonly data: string;\n readonly mimeType: string;\n readonly annotations?: McpAnnotations;\n readonly _meta?: McpMeta;\n }\n | {\n readonly type: \"resource\";\n readonly resource:\n | { readonly uri: string; readonly mimeType?: string; readonly text: string; readonly _meta?: McpMeta }\n | { readonly uri: string; readonly mimeType?: string; readonly blob: string; readonly _meta?: McpMeta };\n readonly annotations?: McpAnnotations;\n readonly _meta?: McpMeta;\n }\n | {\n readonly type: \"resource_link\";\n readonly uri: string;\n readonly name: string;\n readonly title?: string;\n readonly description?: string;\n readonly mimeType?: string;\n readonly size?: number;\n readonly annotations?: McpAnnotations;\n readonly icons?: ReadonlyArray<{\n readonly src: string;\n readonly mimeType?: string;\n readonly sizes?: ReadonlyArray<string>;\n readonly theme?: \"light\" | \"dark\";\n }>;\n readonly _meta?: McpMeta;\n };\n\ninterface McpPresentation {\n /** Suppress automatic presentation for this value and expose its original JSON. */\n raw<T>(value: T): T;\n text(text: string, annotations?: McpAnnotations): McpContentBlock;\n image(data: string, mimeType: string, annotations?: McpAnnotations): McpContentBlock;\n audio(data: string, mimeType: string, annotations?: McpAnnotations): McpContentBlock;\n resource(\n resource:\n | {\n readonly uri: string;\n readonly mimeType?: string;\n readonly text: string;\n readonly _meta?: McpMeta;\n }\n | {\n readonly uri: string;\n readonly mimeType?: string;\n readonly blob: string;\n readonly _meta?: McpMeta;\n },\n annotations?: McpAnnotations,\n ): McpContentBlock;\n resourceLink(\n resource: Omit<Extract<McpContentBlock, { readonly type: \"resource_link\" }>, \"type\">,\n ): McpContentBlock;\n /** Mark any MCP content block for validation when the script result is presented. */\n content(block: McpContentBlock): McpContentBlock;\n /**\n * Return custom MCP content alongside a separate logical value. `value` is\n * what the next call receives as `lastResultVal`.\n */\n result(options: {\n readonly value?: unknown;\n readonly structuredContent?: Record<string, unknown>;\n readonly content?: ReadonlyArray<McpContentBlock>;\n readonly isError?: boolean;\n readonly _meta?: McpMeta;\n }): unknown;\n}\n\ninterface SvcConnection {\n /**\n * Cooperative cancellation signal. When the sandbox is ~200 ms away\n * from its hard timeout (and the configured `timeoutMs` was at least\n * 500 ms), the host flips `aborted` to `true`, sets `reason`, and\n * rejects every in-flight host promise with an `AbortError`. This\n * gives the guest a small grace window to run `finally` blocks,\n * persist partial state, or return a partial result before the hard\n * interrupt kicks in. Subsequent `con.call` / `con.notify` /\n * `con.explore` calls in this window reject immediately so the guest\n * doesn't spend its grace window waiting for another RPC.\n *\n * Idiomatic use:\n *\n * try {\n * return await con.call(s, i, m, params);\n * } catch (e) {\n * if (con.abortSignal.aborted) return { partial: true };\n * throw e;\n * }\n */\n readonly abortSignal: {\n readonly aborted: boolean;\n readonly reason: string | undefined;\n throwIfAborted(): void;\n };\n\n /**\n * Invoke a request on the bus.\n * - Form 3 (most common): pass `serviceId` to address a specific\n * participant: `con.call(\"vscode\", \"vscode.window\", \"showInformationMessage\", { message })`.\n * - Form 2: pass `serviceId: \"\"` to address the hub itself (the\n * root): `con.call(\"\", \"hubDirectory\", \"listPrefixes\")`.\n *\n * Rejects with an `Error` that carries the RPC error message when the\n * peer returns a JSON-RPC error.\n *\n * Access is NOT automatic: a gated member you hold no capability for\n * rejects with a permission error. Check `con.grants()` and request\n * access via `con.requestAccess(...)` first — or pass\n * `{ requestPermission: true }` to have this call auto-negotiate the\n * capability it needs (a single `hubAccess` round-trip) and retry once.\n *\n * Streaming methods can report server-to-client messages while the request\n * is pending:\n *\n * const progress = [];\n * const result = await con.call(\"svc\", \"jobs\", \"run\", {}, {\n * onStreamMessage: message => {\n * progress.push(message);\n * console.log(\"progress\", message);\n * },\n * });\n *\n * When `onStreamMessage` is omitted, each stream message is written to\n * `console.log` with the wire method name. This makes a parked streaming\n * call observable through `awaitLinkRpcTask` without extra callback code.\n */\n call(\n serviceId: string,\n interfaceId: string,\n member: string,\n params?: unknown,\n options?: CallOptions,\n ): Promise<unknown>;\n\n /** Fire-and-forget variant of `call`. Returns once the notification has been sent. */\n notify(\n serviceId: string,\n interfaceId: string,\n member: string,\n params?: unknown,\n ): Promise<void>;\n\n /**\n * Escape hatch when you have a fully-formed JSON-RPC method name (e.g.\n * `\"vscode::vscode.window::showInformationMessage\"`). Prefer `call`.\n */\n callRaw(method: string, params?: unknown, options?: CallOptions): Promise<unknown>;\n notifyRaw(method: string, params?: unknown): Promise<void>;\n\n /**\n * Discover and inspect the interfaces on the bus.\n *\n * - `{ kind: \"browse\" }` lists stable virtual-document identities. Use\n * exact `serviceId` / `interfaceId` filters to narrow the directory.\n * - `{ kind: \"grep\", pattern }` searches the generated, self-contained\n * `defineInterface` source for every candidate. `pattern` is a\n * case-insensitive regular expression by default; set `syntax:\n * \"literal\"` for a case-insensitive substring. Results contain the\n * matching line, nearby source context, and enclosing member name.\n * - `{ kind: \"inspect\", serviceId, interfaceId }` returns one complete\n * generated source document. Set `format: \"schema\"` for its exact raw\n * wire schema instead.\n * - Reflection plumbing (`linkrpc.*`) is hidden unless `includeInternal`\n * is true or an exact internal `interfaceId` is requested.\n * - `requestPermission: true` requests one broad reflection grant and\n * walks gated directories. Without it, inaccessible branches are\n * reported explicitly instead of being silently omitted.\n * - Browse and grep use cursor pagination. Pass a returned `nextCursor`\n * back with the same query to continue.\n */\n explore(args: ExploreArgs): Promise<ExploreResult>;\n\n /**\n * List the capabilities this connection currently holds — what\n * `(serviceId, interfaceId, members)` you may already invoke, each grant's\n * expiry, and whether it is one-shot. Call this BEFORE invoking a gated\n * member to check you have access, and to decide what to pass to\n * `requestAccess`. Reflection (`explore`) works without any grant.\n */\n grants(): Promise<GrantsSummary>;\n\n /**\n * Request one or more capabilities from the hub. Calls are NOT\n * auto-granted: if `con.call(...)` fails with a permission error, ask for\n * access here first, then retry the call.\n *\n * - Pass MANY `permissions` at once to batch related access into a\n * SINGLE user prompt (e.g. several members on one service, or read +\n * write together) instead of nagging once per call.\n * - Choose `duration`: `\"once\"` (single use, 5 min), `\"shortLived\"`\n * (5 min), `\"longLived\"` (24 h), or `\"persistent\"` (never expires,\n * remembered across runs).\n * - Granted durable caps are cached on the connection, so later\n * `con.call`s present them automatically — no need to re-request.\n *\n * `target.serviceId` / `interfaceId` / `members` are matchers:\n * `{ exact: \"x\" }` or `{ prefix: \"x\" }` (`{ prefix: \"\" }` = any).\n * Set `canInvoke: true` to actually call the members.\n *\n * con.requestAccess({\n * permissions: [{\n * target: {\n * serviceId: { exact: \"github\" },\n * interfaceId: { exact: \"github.issues\" },\n * members: [{ exact: \"list\" }, { exact: \"get\" }],\n * },\n * canInvoke: true,\n * }],\n * duration: \"longLived\",\n * purpose: \"read the open repo's issues\",\n * })\n */\n requestAccess(args: RequestAccessArgs): Promise<RequestAccessResult>;\n\n /**\n * Return this very text (the TypeScript declarations for the full script context).\n * Synchronous, no host round-trip — just an embedded string. Useful\n * when you want to re-read the API contract before composing a more\n * complicated call.\n */\n getDocs(): string;\n}\n\n// ---- access (capabilities) ----------------------------------------------\n\n/** Per-call options for {@link SvcConnection.call} / {@link SvcConnection.callRaw}. */\ninterface CallOptions {\n /**\n * When `true` and the call is rejected because no capability is held\n * (`permissionRequired`), automatically request access for exactly this\n * `(serviceId, interfaceId, member)` — a single `hubAccess` round-trip —\n * and retry the call once. The granted capability is durable\n * (`longLived`), so later calls to the same member present it\n * automatically. On a gated hub this may prompt the user.\n *\n * Defaults to `false`: a missing capability surfaces as an error directing\n * you to `con.requestAccess(...)`.\n */\n readonly requestPermission?: boolean;\n /**\n * Called for each server-to-client stream message associated with this\n * request. Messages are delivered as they arrive, including while a script\n * is parked as a background task. Throwing from the callback fails the\n * sandbox and cancels its outstanding requests. When omitted, messages are\n * logged automatically.\n */\n readonly onStreamMessage?: (message: unknown) => void;\n}\n\n/**\n * Sandbox-owned timers. Pending timers continue to run while a script is\n * parked and are cancelled automatically when it completes, fails, is\n * cancelled, or reaches its maximum lifetime.\n */\ndeclare function setTimeout(\n callback: (...args: unknown[]) => void,\n timeout?: number,\n ...args: unknown[]\n): number;\ndeclare function clearTimeout(timerId: number | undefined): void;\ndeclare function setInterval(\n callback: (...args: unknown[]) => void,\n timeout?: number,\n ...args: unknown[]\n): number;\ndeclare function clearInterval(timerId: number | undefined): void;\n\n/** A `serviceId` / `interfaceId` / member matcher. `{ prefix: \"\" }` matches anything. */\ntype AccessPattern = { readonly exact: string } | { readonly prefix: string };\n\ninterface AccessPermission {\n readonly target: {\n readonly serviceId: AccessPattern;\n readonly interfaceId: AccessPattern;\n readonly interfaceHash?: string;\n /** `[{ prefix: \"\" }]` covers every member. */\n readonly members: ReadonlyArray<AccessPattern>;\n };\n /** Set true to actually invoke the members (default false). */\n readonly canInvoke?: boolean;\n /** Set true to allow re-delegating this authority (default false). */\n readonly canDelegate?: boolean;\n readonly params?: Record<string, unknown>;\n}\n\ninterface RequestAccessArgs {\n /** One or more authorities to request together in a single prompt. */\n readonly permissions: ReadonlyArray<AccessPermission>;\n readonly duration?: \"once\" | \"shortLived\" | \"longLived\" | \"persistent\";\n /** Short human-readable reason shown to the user. */\n readonly purpose?: string;\n}\n\ntype RequestAccessResult =\n | { readonly status: \"granted\"; readonly capabilities: ReadonlyArray<unknown>; readonly addedDurable: number }\n | { readonly status: \"denied\"; readonly reason?: string }\n | { readonly status: string; readonly reason?: string };\n\ninterface GrantPermissionSummary {\n readonly serviceId: string;\n readonly interfaceId: string;\n readonly members: ReadonlyArray<string>;\n readonly canInvoke: boolean;\n readonly canDelegate: boolean;\n}\n\ninterface GrantSummary {\n readonly issuer: string;\n readonly audience: string;\n readonly expiresAtMs?: number;\n /** True when pinned to a single call (`callBind`); not reusable. */\n readonly oneShot: boolean;\n readonly permissions: ReadonlyArray<GrantPermissionSummary>;\n}\n\ninterface GrantsSummary {\n readonly count: number;\n readonly grants: ReadonlyArray<GrantSummary>;\n}\n\ninterface ExploreCommonArgs {\n /** Exact directory-level filters, applied before browsing or searching. */\n readonly serviceId?: string;\n readonly interfaceId?: string;\n /** Include reflection plumbing such as `hubrpc.directory` and `hubrpc.schemas`. */\n readonly includeInternal?: boolean;\n /** Request one broad reflection grant when a gated directory is encountered. */\n readonly requestPermission?: boolean;\n}\n\ninterface ExploreBrowseArgs extends ExploreCommonArgs {\n readonly kind: \"browse\";\n /** Number of interfaces to return. Defaults to 20; maximum 100. */\n readonly limit?: number;\n /** Continuation cursor returned by a previous browse call. */\n readonly cursor?: string;\n}\n\ninterface ExploreGrepArgs extends ExploreCommonArgs {\n readonly kind: \"grep\";\n /** Pattern searched against generated `defineInterface` source, one line at a time. */\n readonly pattern: string;\n /** Defaults to `regex`. Both modes are case-insensitive. */\n readonly syntax?: \"regex\" | \"literal\";\n /** Number of matching virtual documents to return. Defaults to 20; maximum 100. */\n readonly limit?: number;\n /** Continuation cursor returned by a previous grep call. */\n readonly cursor?: string;\n /** Source lines included before and after each match. Defaults to 1; maximum 5. */\n readonly contextLines?: number;\n}\n\ninterface ExploreInspectArgs extends ExploreCommonArgs {\n readonly kind: \"inspect\";\n readonly serviceId: string;\n readonly interfaceId: string;\n /** Generated source by default; use `schema` for the raw wire schema. */\n readonly format?: \"source\" | \"schema\";\n}\n\ntype ExploreArgs = ExploreBrowseArgs | ExploreGrepArgs | ExploreInspectArgs;\n\ntype ExploreResult = ExploreBrowseResult | ExploreGrepResult | ExploreInspectResult;\n\ninterface ExploreListing {\n readonly serviceId: string;\n readonly serviceDescription?: string;\n readonly interfaceId: string;\n readonly interfaceHash: string;\n readonly documentId: string;\n}\n\ninterface ExploreBrowseResult {\n readonly kind: \"browse\";\n readonly total: number;\n readonly entries: ReadonlyArray<ExploreListing>;\n readonly nextCursor?: string;\n readonly inaccessible?: ReadonlyArray<ExploreInaccessible>;\n}\n\ninterface ExploreGrepMatch {\n /** Matching source plus the requested surrounding context, joined with newlines. */\n readonly searchResult: string;\n /** Inclusive, 1-based line range of `searchResult` in the virtual document. */\n readonly lineRange: readonly [start: number, end: number];\n /** LinkRPC member containing every matched line in this chunk, when unambiguous. */\n readonly member?: string;\n}\n\ninterface ExploreGrepEntry extends ExploreListing {\n readonly matches: ReadonlyArray<ExploreGrepMatch>;\n readonly matchesTruncated?: boolean;\n}\n\ninterface ExploreDocumentError {\n readonly serviceId: string;\n readonly interfaceId: string;\n readonly error: string;\n}\n\ninterface ExploreGrepResult {\n readonly kind: \"grep\";\n readonly pattern: string;\n readonly syntax: \"regex\" | \"literal\";\n readonly total: number;\n readonly entries: ReadonlyArray<ExploreGrepEntry>;\n readonly nextCursor?: string;\n readonly documentErrors?: ReadonlyArray<ExploreDocumentError>;\n readonly inaccessible?: ReadonlyArray<ExploreInaccessible>;\n}\n\ninterface ExploreInspectResult extends ExploreListing {\n readonly kind: \"inspect\";\n readonly format: \"source\" | \"schema\";\n readonly source?: string;\n readonly schema?: unknown;\n readonly inaccessible?: ReadonlyArray<ExploreInaccessible>;\n}\n\ninterface ExploreInaccessible {\n /** The directory target (serviceId) that could not be enumerated. */\n readonly serviceId: string;\n /** The error message from the denied directory lookup. */\n readonly reason: string;\n /** Actionable next step to gain visibility into this directory. */\n readonly hint: string;\n}\n\n// ---- console -------------------------------------------------------------\n// `console.log`, `console.warn`, `console.error` are captured and\n// returned alongside the tool result. There is no `console.debug` /\n// `console.trace` etc.\ndeclare const console: {\n log(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---- Notes ---------------------------------------------------------------\n// - Everything is JSON-serialisable. Functions, dates, etc. become `null`\n// when crossing the sandbox boundary.\n// - The user code MUST evaluate to a function. Top-level statements are\n// wrapped, so write an arrow or function expression, not a bare\n// `await con.call(...)`.\n// - `throw` inside user code surfaces as a failed tool call.\n";
821
+ //#endregion
822
+ //#region src/sandbox.ts
823
+ /**
824
+ * The guest runtime (`src/guest/guestMain.ts`) bundled to plain JS, evaluated
825
+ * inside QuickJS before any user code. Authoring it as real TypeScript that is
826
+ * type-checked against `connection.d.ts` is what keeps `con` from drifting out
827
+ * of sync with its declarations.
828
+ *
829
+ * `@vscode/rollup-plugin-esm-url` rewrites the `?esm` URL at build time to the
830
+ * emitted (already-transpiled) chunk next to this module, so the production
831
+ * read returns JS. In dev/test (vite serve, no rewrite) the URL still points
832
+ * at the `.ts` source, so we transpile it on the fly with the `typescript`
833
+ * devDependency — that branch never runs in the shipped `dist`.
834
+ */
835
+ function _loadGuestRuntime() {
836
+ const filePath = fileURLToPath(new URL("../src-guest-guestMain.js?esm", import.meta.url));
837
+ const code = readFileSync(filePath, "utf8");
838
+ if (!filePath.endsWith(".ts")) return _asGuestScript(code);
839
+ const ts = createRequire(import.meta.url)("typescript");
840
+ const transpiled = ts.transpileModule(code, { compilerOptions: {
841
+ target: ts.ScriptTarget.ES2022,
842
+ module: ts.ModuleKind.ESNext
843
+ } }).outputText;
844
+ return _asGuestScript(transpiled);
845
+ }
846
+ function _asGuestScript(code) {
847
+ return code.replace(/^\s*export\s*\{\s*\};?\s*$/m, "");
848
+ }
849
+ const GUEST_RUNTIME_JS = _loadGuestRuntime();
850
+ const DEFAULT_FOREGROUND_MS = 5e3;
851
+ const DEFAULT_MAX_LIFETIME_MS = 12e4;
852
+ const DEFAULT_MEMORY_LIMIT_BYTES = 33554432;
853
+ /**
854
+ * Grace window granted to the guest after a soft abort (cancel / max
855
+ * lifetime) so it can run `catch`/`finally` and return a partial result
856
+ * before the task is force-settled.
857
+ */
858
+ const SOFT_CANCEL_GRACE_MS = 200;
859
+ let _quickjsPromise;
860
+ function _qjs() {
861
+ if (!_quickjsPromise) _quickjsPromise = getQuickJS();
862
+ return _quickjsPromise;
863
+ }
864
+ const _delay = () => new Promise((r) => setImmediate(r));
865
+ /**
866
+ * Run-to-completion entry point preserved for callers that just want a result
867
+ * and treat the timeout as a hard wall (the original behaviour). Throws on
868
+ * guest error or deadline; never parks.
869
+ */
870
+ async function runSandboxed(userCode, host, lastResultVal, options = {}) {
871
+ const budget = options.timeoutMs ?? DEFAULT_FOREGROUND_MS;
872
+ const outcome = await (await SandboxExecution.create(userCode, host, lastResultVal, {
873
+ foregroundMs: budget,
874
+ maxLifetimeMs: budget,
875
+ memoryLimitBytes: options.memoryLimitBytes
876
+ })).runForeground();
877
+ if (outcome.status === "completed") return {
878
+ resultJson: outcome.result === void 0 ? "" : JSON.stringify(outcome.result),
879
+ logs: outcome.logs
880
+ };
881
+ if (outcome.status === "error") throw new Error(outcome.error);
882
+ const final = await outcome.task.cancel();
883
+ throw new Error(final.status === "error" ? final.error : "sandbox deadline exceeded");
884
+ }
885
+ /**
886
+ * Parking-capable entry point. Runs `userCode` for up to `foregroundMs`; if it
887
+ * settles in that window the result is returned inline, otherwise the live VM
888
+ * is detached into a {@link ParkedTask} that keeps pumping in the background.
889
+ */
890
+ async function startSandbox(userCode, host, lastResultVal, options = {}) {
891
+ return (await SandboxExecution.create(userCode, host, lastResultVal, {
892
+ foregroundMs: options.foregroundMs ?? DEFAULT_FOREGROUND_MS,
893
+ maxLifetimeMs: options.maxLifetimeMs ?? DEFAULT_MAX_LIFETIME_MS,
894
+ memoryLimitBytes: options.memoryLimitBytes,
895
+ label: options.label
896
+ })).runForeground();
897
+ }
898
+ /**
899
+ * Encapsulates one QuickJS context and drives it through a foreground phase
900
+ * and (optionally) a background phase. Only one driver loop runs at a time:
901
+ * `runForeground` runs first and, if it parks, hands off to `_runBackground`.
902
+ * `cancel` cooperates with the background loop rather than starting its own.
903
+ */
904
+ var SandboxExecution = class SandboxExecution {
905
+ _userCode;
906
+ _host;
907
+ _lastResultVal;
908
+ _options;
909
+ static async create(userCode, host, lastResultVal, options) {
910
+ const QuickJS = await _qjs();
911
+ const exec = new SandboxExecution(userCode, host, lastResultVal, options);
912
+ try {
913
+ exec._init(QuickJS);
914
+ } catch (e) {
915
+ exec._dispose();
916
+ throw e;
917
+ }
918
+ return exec;
919
+ }
920
+ _scope = new Scope();
921
+ _logs = [];
922
+ /** In-flight host->guest deferreds keyed by a human-readable call label. */
923
+ _pendingDeferreds = /* @__PURE__ */ new Map();
924
+ _pendingTimers = /* @__PURE__ */ new Map();
925
+ _vmState = {
926
+ alive: true,
927
+ softAborted: false,
928
+ softAbortReason: void 0
929
+ };
930
+ _abortController = new AbortController();
931
+ _cpuBurstMs;
932
+ _startedAt = Date.now();
933
+ _runtime;
934
+ _vm;
935
+ _promiseH;
936
+ /** Deadline for the current synchronous burst; read by the interrupt handler. */
937
+ _cpuDeadline = 0;
938
+ _disposed = false;
939
+ _settled;
940
+ _backgroundDone;
941
+ _cancelGraceDeadline;
942
+ constructor(_userCode, _host, _lastResultVal, _options) {
943
+ this._userCode = _userCode;
944
+ this._host = _host;
945
+ this._lastResultVal = _lastResultVal;
946
+ this._options = _options;
947
+ this._cpuBurstMs = Math.max(_options.foregroundMs, 50);
948
+ }
949
+ get _allowPark() {
950
+ return this._options.maxLifetimeMs > this._options.foregroundMs;
951
+ }
952
+ _init(QuickJS) {
953
+ const runtime = this._scope.manage(QuickJS.newRuntime());
954
+ runtime.setMemoryLimit(this._options.memoryLimitBytes ?? DEFAULT_MEMORY_LIMIT_BYTES);
955
+ runtime.setInterruptHandler(() => Date.now() > this._cpuDeadline);
956
+ this._runtime = runtime;
957
+ this._vm = this._scope.manage(runtime.newContext());
958
+ this._cpuDeadline = Date.now() + this._cpuBurstMs;
959
+ this._installHostApi();
960
+ _evalPrelude(this._vm, this._lastResultVal);
961
+ }
962
+ async runForeground() {
963
+ this._cpuDeadline = Date.now() + this._cpuBurstMs;
964
+ const evalRes = this._vm.evalCode(_wrapUserCode(this._userCode));
965
+ if (evalRes.error) {
966
+ const dumped = this._vm.dump(evalRes.error);
967
+ evalRes.error.dispose();
968
+ this._forceSettle({
969
+ status: "error",
970
+ error: _formatGuestError(dumped),
971
+ logs: this._logs.slice()
972
+ });
973
+ return this._terminalOutcome();
974
+ }
975
+ this._promiseH = evalRes.value;
976
+ this._pump();
977
+ if (await this._loopUntil(Date.now() + this._options.foregroundMs) === "settled") return this._terminalOutcome();
978
+ if (!this._allowPark) {
979
+ this._triggerSoftAbort("sandbox deadline exceeded");
980
+ await this._loopUntil(Date.now() + SOFT_CANCEL_GRACE_MS);
981
+ if (!this._settled) this._forceSettle({
982
+ status: "error",
983
+ error: "sandbox deadline exceeded",
984
+ logs: this._logs.slice()
985
+ });
986
+ return this._terminalOutcome();
987
+ }
988
+ const debugName = this._computeDebugName();
989
+ this._backgroundDone = this._runBackground();
990
+ return {
991
+ status: "parked",
992
+ debugName,
993
+ task: this._asParkedTask()
994
+ };
995
+ }
996
+ async _runBackground() {
997
+ const hardDeadline = this._startedAt + this._options.maxLifetimeMs;
998
+ while (true) {
999
+ if (this._settled || this._checkPromise()) break;
1000
+ const now = Date.now();
1001
+ if (this._cancelGraceDeadline !== void 0 && now > this._cancelGraceDeadline) {
1002
+ this._forceSettle({
1003
+ status: "cancelled",
1004
+ logs: this._logs.slice()
1005
+ });
1006
+ break;
1007
+ }
1008
+ if (now > hardDeadline) {
1009
+ this._triggerSoftAbort("max lifetime exceeded");
1010
+ this._forceSettle({
1011
+ status: "error",
1012
+ error: "sandbox max lifetime exceeded",
1013
+ logs: this._logs.slice()
1014
+ });
1015
+ break;
1016
+ }
1017
+ await _delay();
1018
+ this._pump();
1019
+ }
1020
+ return this._settled;
1021
+ }
1022
+ /** Pump + poll the user promise until it settles or `deadline` passes. */
1023
+ async _loopUntil(deadline) {
1024
+ while (true) {
1025
+ if (this._settled || this._checkPromise()) return "settled";
1026
+ if (Date.now() > deadline) return "pending";
1027
+ await _delay();
1028
+ this._pump();
1029
+ }
1030
+ }
1031
+ /** Returns true once the user promise has settled (and disposes the VM). */
1032
+ _checkPromise() {
1033
+ if (this._settled) return true;
1034
+ if (!this._vmState.alive || !this._promiseH) return true;
1035
+ const state = this._vm.getPromiseState(this._promiseH);
1036
+ if (state.type === "fulfilled") {
1037
+ let json;
1038
+ try {
1039
+ json = this._vm.getString(state.value);
1040
+ } finally {
1041
+ state.value.dispose();
1042
+ }
1043
+ const result = json === "" ? void 0 : JSON.parse(json);
1044
+ this._forceSettle({
1045
+ status: "completed",
1046
+ result,
1047
+ logs: this._logs.slice()
1048
+ });
1049
+ return true;
1050
+ }
1051
+ if (state.type === "rejected") {
1052
+ const dumped = this._vm.dump(state.error);
1053
+ state.error.dispose();
1054
+ const e = _toError(dumped);
1055
+ this._settleError(e.message, _formatGuestError(dumped));
1056
+ return true;
1057
+ }
1058
+ return false;
1059
+ }
1060
+ _settleError(message, detail) {
1061
+ if (this._vmState.softAborted && /abort/i.test(message)) {
1062
+ this._forceSettle({
1063
+ status: "cancelled",
1064
+ logs: this._logs.slice()
1065
+ });
1066
+ return;
1067
+ }
1068
+ const normalized = /interrupt/i.test(message) ? "sandbox deadline exceeded" : detail ?? message;
1069
+ this._forceSettle({
1070
+ status: "error",
1071
+ error: normalized,
1072
+ logs: this._logs.slice()
1073
+ });
1074
+ }
1075
+ _forceSettle(outcome) {
1076
+ if (this._settled) return;
1077
+ this._dispose();
1078
+ this._settled = outcome;
1079
+ }
1080
+ _pump() {
1081
+ if (!this._vmState.alive || this._disposed) return;
1082
+ this._cpuDeadline = Date.now() + this._cpuBurstMs;
1083
+ const r = this._runtime.executePendingJobs();
1084
+ if (r.error) {
1085
+ const dumped = this._vm.dump(r.error);
1086
+ r.error.dispose();
1087
+ const e = _toError(dumped);
1088
+ this._settleError(e.message, _formatGuestError(dumped));
1089
+ }
1090
+ }
1091
+ _computeDebugName() {
1092
+ if (this._options.label) return this._options.label;
1093
+ const inflight = this._inFlight();
1094
+ if (inflight.length > 0) return inflight.join(", ");
1095
+ return _firstLine(this._userCode);
1096
+ }
1097
+ _inFlight() {
1098
+ return [...this._pendingDeferreds.values(), ...[...this._pendingTimers.values()].map((timer) => `${timer.repeat ? "setInterval" : "setTimeout"}(${timer.delayMs}ms)`)];
1099
+ }
1100
+ _asParkedTask() {
1101
+ return {
1102
+ inFlight: () => this._inFlight(),
1103
+ logs: this._logs,
1104
+ done: this._backgroundDone,
1105
+ cancel: () => this.cancel()
1106
+ };
1107
+ }
1108
+ async cancel() {
1109
+ if (this._settled) return this._settled;
1110
+ this._triggerSoftAbort("cancelled");
1111
+ this._cancelGraceDeadline = Date.now() + SOFT_CANCEL_GRACE_MS;
1112
+ if (this._backgroundDone) return this._backgroundDone;
1113
+ this._forceSettle({
1114
+ status: "cancelled",
1115
+ logs: this._logs.slice()
1116
+ });
1117
+ return this._settled;
1118
+ }
1119
+ /**
1120
+ * Flip the guest-visible `con.abortSignal.aborted` flag and reject every
1121
+ * in-flight host promise with an `AbortError` naming the call.
1122
+ */
1123
+ _triggerSoftAbort(reason) {
1124
+ if (this._vmState.softAborted || !this._vmState.alive) return;
1125
+ this._vmState.softAborted = true;
1126
+ this._vmState.softAbortReason = reason;
1127
+ this._abortController.abort(reason);
1128
+ this._clearAllTimers();
1129
+ this._cpuDeadline = Date.now() + this._cpuBurstMs;
1130
+ const setFlag = this._vm.evalCode(`(() => {
1131
+ if (globalThis.con && globalThis.con.abortSignal) {
1132
+ globalThis.con.abortSignal.aborted = true;
1133
+ globalThis.con.abortSignal.reason = ${JSON.stringify(reason)};
1134
+ }
1135
+ })()`);
1136
+ if (setFlag.error) setFlag.error.dispose();
1137
+ else setFlag.value.dispose();
1138
+ const entries = [...this._pendingDeferreds];
1139
+ this._pendingDeferreds.clear();
1140
+ for (const [d, label] of entries) {
1141
+ if (!d.alive) continue;
1142
+ this._vm.newError(`AbortError: ${reason} (cancelled in-flight: ${label})`).consume((errH) => d.reject(errH));
1143
+ }
1144
+ this._pump();
1145
+ }
1146
+ _dispose() {
1147
+ if (this._disposed) return;
1148
+ this._disposed = true;
1149
+ this._vmState.alive = false;
1150
+ if (!this._abortController.signal.aborted) this._abortController.abort("sandbox disposed");
1151
+ this._clearAllTimers();
1152
+ for (const d of this._pendingDeferreds.keys()) if (d.alive) d.dispose();
1153
+ this._pendingDeferreds.clear();
1154
+ if (this._promiseH && this._promiseH.alive) this._promiseH.dispose();
1155
+ this._scope.dispose();
1156
+ }
1157
+ _terminalOutcome() {
1158
+ const o = this._settled;
1159
+ if (!o) throw new Error("sandbox: terminal outcome requested before settling");
1160
+ if (o.status === "completed") return {
1161
+ status: "completed",
1162
+ result: o.result,
1163
+ logs: o.logs
1164
+ };
1165
+ if (o.status === "cancelled") return {
1166
+ status: "error",
1167
+ error: "sandbox cancelled",
1168
+ logs: o.logs
1169
+ };
1170
+ return {
1171
+ status: "error",
1172
+ error: o.error,
1173
+ logs: o.logs
1174
+ };
1175
+ }
1176
+ _installHostApi() {
1177
+ this._registerAsyncHostFn("__hostCall", (method, paramsJson, optsJson, emit) => this._host.call(method, paramsJson, optsJson, emit, this._abortController.signal), (method) => `con.call(${JSON.stringify(method)})`, "__dispatchHostStream");
1178
+ this._registerAsyncHostFn("__hostNotify", (m, p) => this._host.notify(m, p), (method) => `con.notify(${JSON.stringify(method)})`);
1179
+ this._registerAsyncHostFn("__hostExplore", (a) => this._host.explore(a), () => `con.explore(...)`);
1180
+ this._registerAsyncHostFn("__hostRequestAccess", (a) => this._host.requestAccess(a), () => `con.requestAccess(...)`);
1181
+ this._registerAsyncHostFn("__hostGrants", (a) => this._host.grants(a), () => `con.grants()`);
1182
+ this._vm.newFunction("__hostLog", (levelH, textH) => {
1183
+ const level = this._vm.getString(levelH);
1184
+ const text = textH ? this._vm.getString(textH) : "";
1185
+ this._logs.push({
1186
+ level,
1187
+ text
1188
+ });
1189
+ }).consume((fn) => this._vm.setProp(this._vm.global, "__hostLog", fn));
1190
+ this._vm.newFunction("__hostSetTimer", (idH, delayH, repeatH) => {
1191
+ this._setTimer(this._vm.getNumber(idH), this._vm.getNumber(delayH), this._vm.getNumber(repeatH) === 1);
1192
+ }).consume((fn) => this._vm.setProp(this._vm.global, "__hostSetTimer", fn));
1193
+ this._vm.newFunction("__hostClearTimer", (idH) => {
1194
+ this._clearTimer(this._vm.getNumber(idH));
1195
+ }).consume((fn) => this._vm.setProp(this._vm.global, "__hostClearTimer", fn));
1196
+ }
1197
+ _setTimer(timerId, delayMs, repeat) {
1198
+ this._clearTimer(timerId);
1199
+ if (!this._vmState.alive || this._vmState.softAborted) return;
1200
+ const normalizedDelay = Math.min(Math.max(0, Math.floor(delayMs)), 2147483647);
1201
+ const callback = () => {
1202
+ if (!this._vmState.alive || this._disposed) return;
1203
+ if (!repeat) this._pendingTimers.delete(timerId);
1204
+ this._dispatchGuestFunction("__dispatchHostTimer", timerId);
1205
+ };
1206
+ const handle = repeat ? setInterval(callback, normalizedDelay) : setTimeout(callback, normalizedDelay);
1207
+ this._pendingTimers.set(timerId, {
1208
+ handle,
1209
+ repeat,
1210
+ delayMs: normalizedDelay
1211
+ });
1212
+ }
1213
+ _clearTimer(timerId) {
1214
+ const timer = this._pendingTimers.get(timerId);
1215
+ if (!timer) return;
1216
+ if (timer.repeat) clearInterval(timer.handle);
1217
+ else clearTimeout(timer.handle);
1218
+ this._pendingTimers.delete(timerId);
1219
+ }
1220
+ _clearAllTimers() {
1221
+ for (const timerId of [...this._pendingTimers.keys()]) this._clearTimer(timerId);
1222
+ }
1223
+ _dispatchGuestFunction(name, ...args) {
1224
+ if (!this._vmState.alive || this._disposed || this._settled) return;
1225
+ this._cpuDeadline = Date.now() + this._cpuBurstMs;
1226
+ const result = this._vm.evalCode(`globalThis[${JSON.stringify(name)}](...${JSON.stringify(args)})`);
1227
+ if (result.error) {
1228
+ const dumped = this._vm.dump(result.error);
1229
+ result.error.dispose();
1230
+ const error = _toError(dumped);
1231
+ this._settleError(error.message, _formatGuestError(dumped));
1232
+ return;
1233
+ }
1234
+ result.value.dispose();
1235
+ this._pump();
1236
+ }
1237
+ _registerAsyncHostFn(name, impl, label, guestEventHandler) {
1238
+ this._vm.newFunction(name, (aH, bH, cH, eventIdH) => {
1239
+ const a = aH ? this._vm.getString(aH) : "";
1240
+ const b = bH ? this._vm.getString(bH) : "";
1241
+ const c = cH ? this._vm.getString(cH) : "";
1242
+ const eventId = eventIdH ? this._vm.getString(eventIdH) : "";
1243
+ const deferred = this._vm.newPromise();
1244
+ if (this._vmState.softAborted) {
1245
+ const reason = this._vmState.softAbortReason ?? "soft deadline reached";
1246
+ this._vm.newError(`AbortError: ${reason} (refused: ${label(a, b, c)})`).consume((errH) => deferred.reject(errH));
1247
+ deferred.settled.then(() => this._pump());
1248
+ return deferred.handle;
1249
+ }
1250
+ this._pendingDeferreds.set(deferred, label(a, b, c));
1251
+ impl(a, b, c, (payloadJson) => {
1252
+ if (guestEventHandler && eventId !== "") this._dispatchGuestFunction(guestEventHandler, eventId, payloadJson);
1253
+ }).then((value) => {
1254
+ this._pendingDeferreds.delete(deferred);
1255
+ if (!this._vmState.alive || !deferred.alive) return;
1256
+ this._vm.newString(value).consume((sH) => deferred.resolve(sH));
1257
+ }, (err) => {
1258
+ this._pendingDeferreds.delete(deferred);
1259
+ if (!this._vmState.alive || !deferred.alive) return;
1260
+ this._newGuestHostError(err).consume((errH) => deferred.reject(errH));
1261
+ });
1262
+ deferred.settled.then(() => this._pump());
1263
+ return deferred.handle;
1264
+ }).consume((fn) => this._vm.setProp(this._vm.global, name, fn));
1265
+ }
1266
+ _newGuestHostError(thrown) {
1267
+ const snapshot = _snapshotHostRejection(thrown);
1268
+ const source = `(() => {
1269
+ const error = new Error(${JSON.stringify(snapshot.summary)});
1270
+ const properties = JSON.parse(${JSON.stringify(JSON.stringify(snapshot.properties))});
1271
+ for (const [key, value] of Object.entries(properties)) {
1272
+ Object.defineProperty(error, key, {
1273
+ value,
1274
+ writable: true,
1275
+ enumerable: true,
1276
+ configurable: true,
1277
+ });
1278
+ }
1279
+ return error;
1280
+ })()`;
1281
+ const result = this._vm.evalCode(source);
1282
+ if (result.error) {
1283
+ result.error.dispose();
1284
+ return this._vm.newError(snapshot.summary);
1285
+ }
1286
+ return result.value;
1287
+ }
1288
+ };
1289
+ const MAX_REJECTION_DEPTH = 8;
1290
+ const MAX_REJECTION_PROPERTIES = 100;
1291
+ const MAX_REJECTION_ARRAY_ITEMS = 100;
1292
+ const MAX_REJECTION_STRING_LENGTH = 2e4;
1293
+ const MAX_REJECTION_NODES = 1e3;
1294
+ const MAX_REJECTION_TOTAL_STRING_LENGTH = 1e5;
1295
+ function _snapshotHostRejection(thrown) {
1296
+ return {
1297
+ summary: _rejectionSummary(thrown),
1298
+ properties: _snapshotOwnDataProperties(thrown)
1299
+ };
1300
+ }
1301
+ function _rejectionSummary(thrown) {
1302
+ if (thrown === null) return "Host operation rejected with null";
1303
+ if (typeof thrown !== "object" && typeof thrown !== "function") return `Host operation rejected with ${String(thrown)}`;
1304
+ return "Host operation failed";
1305
+ }
1306
+ function _snapshotOwnDataProperties(value) {
1307
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") return { value: _snapshotBridgeValue(value, /* @__PURE__ */ new WeakSet(), 0, _newSnapshotBudget()) };
1308
+ let descriptors;
1309
+ try {
1310
+ descriptors = Object.getOwnPropertyDescriptors(value);
1311
+ } catch {
1312
+ return {};
1313
+ }
1314
+ const seen = /* @__PURE__ */ new WeakSet();
1315
+ seen.add(value);
1316
+ const budget = _newSnapshotBudget();
1317
+ const result = {};
1318
+ for (const [key, descriptor] of Object.entries(descriptors).slice(0, MAX_REJECTION_PROPERTIES)) {
1319
+ if (!("value" in descriptor)) continue;
1320
+ result[key] = _snapshotBridgeValue(descriptor.value, seen, 0, budget);
1321
+ }
1322
+ return result;
1323
+ }
1324
+ function _newSnapshotBudget() {
1325
+ return {
1326
+ nodes: MAX_REJECTION_NODES,
1327
+ stringChars: MAX_REJECTION_TOTAL_STRING_LENGTH
1328
+ };
1329
+ }
1330
+ function _snapshotBridgeValue(value, seen, depth, budget) {
1331
+ if (budget.nodes <= 0) return "[Truncated]";
1332
+ budget.nodes--;
1333
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
1334
+ if (typeof value === "string") {
1335
+ const available = Math.min(MAX_REJECTION_STRING_LENGTH, budget.stringChars);
1336
+ budget.stringChars -= Math.min(value.length, available);
1337
+ return value.length <= available ? value : `${value.slice(0, available)}…`;
1338
+ }
1339
+ if (typeof value === "undefined") return "[undefined]";
1340
+ if (typeof value === "bigint") return `${value}n`;
1341
+ if (typeof value === "symbol") return String(value);
1342
+ if (typeof value === "function") return `[Function${value.name ? `: ${value.name}` : ""}]`;
1343
+ if (depth >= MAX_REJECTION_DEPTH) return "[Truncated]";
1344
+ if (seen.has(value)) return "[Circular]";
1345
+ seen.add(value);
1346
+ if (Array.isArray(value)) return value.slice(0, MAX_REJECTION_ARRAY_ITEMS).map((item) => _snapshotBridgeValue(item, seen, depth + 1, budget));
1347
+ let descriptors;
1348
+ try {
1349
+ descriptors = Object.getOwnPropertyDescriptors(value);
1350
+ } catch {
1351
+ return "[Uninspectable]";
1352
+ }
1353
+ const result = {};
1354
+ for (const [key, descriptor] of Object.entries(descriptors).slice(0, MAX_REJECTION_PROPERTIES)) {
1355
+ if (!("value" in descriptor)) continue;
1356
+ result[key] = _snapshotBridgeValue(descriptor.value, seen, depth + 1, budget);
1357
+ }
1358
+ return result;
1359
+ }
1360
+ function _evalPrelude(vm, lastResultVal) {
1361
+ const header = `globalThis.__lastResultVal = JSON.parse(${JSON.stringify(JSON.stringify(lastResultVal ?? null))});\nglobalThis.__docs = ${JSON.stringify(CONNECTION_DTS)};\n`;
1362
+ const r = vm.evalCode(header + GUEST_RUNTIME_JS);
1363
+ if (r.error) {
1364
+ const err = vm.dump(r.error);
1365
+ r.error.dispose();
1366
+ throw new Error(`sandbox prelude failed: ${JSON.stringify(err)}`);
1367
+ }
1368
+ r.value.dispose();
1369
+ }
1370
+ function _wrapUserCode(userCode) {
1371
+ return `(async () => {
1372
+ const __userFn = (${userCode});
1373
+ if (typeof __userFn !== "function") {
1374
+ throw new Error("runLinkRpcScript: \`code\` must evaluate to a function, got " + typeof __userFn);
1375
+ }
1376
+ const __r = await __userFn({ con, lastResultVal: __lastResultVal, mcp });
1377
+ return __r === undefined ? "" : JSON.stringify(__r);
1378
+ })()`;
1379
+ }
1380
+ function _firstLine(code) {
1381
+ const line = code.split("\n", 1)[0].trim();
1382
+ return line.length > 80 ? `${line.slice(0, 79)}…` : line;
1383
+ }
1384
+ function _toError(dumped) {
1385
+ let json;
1386
+ try {
1387
+ json = JSON.stringify(dumped);
1388
+ } catch {
1389
+ json = String(dumped);
1390
+ }
1391
+ if (dumped && typeof dumped === "object") {
1392
+ const d = dumped;
1393
+ const message = typeof d.message === "string" && d.message.length > 0 ? d.message : `guest error (raw): ${json}`;
1394
+ const e = new Error(message);
1395
+ if (typeof d.stack === "string") e.stack = d.stack;
1396
+ return e;
1397
+ }
1398
+ return /* @__PURE__ */ new Error(`guest error: ${json}`);
1399
+ }
1400
+ /**
1401
+ * Format a dumped guest error as `message` plus its guest stack (when QuickJS
1402
+ * provided one). The host only forwards the `error` string to the caller, so
1403
+ * folding the stack in here is the only way the guest trace ever reaches them.
1404
+ */
1405
+ function _formatGuestError(dumped) {
1406
+ const e = _toError(dumped);
1407
+ const stack = e.stack;
1408
+ if (typeof stack === "string" && stack.length > 0) return stack.includes(e.message) ? stack : `${e.message}\n${stack}`;
1409
+ return e.message;
1410
+ }
1411
+ //#endregion
1412
+ //#region src/taskRegistry.ts
1413
+ /**
1414
+ * Tracks parked sandbox tasks. At most one task is "live" at a time across all
1415
+ * connections: starting a new one supersedes (cancels) the previous one via
1416
+ * {@link cancelLive}. Settled tasks are retained so their result can still be
1417
+ * fetched by id, and pruned opportunistically.
1418
+ */
1419
+ var TaskRegistry = class {
1420
+ _maxSettledAgeMs;
1421
+ _byId = /* @__PURE__ */ new Map();
1422
+ _live;
1423
+ _seq = 0;
1424
+ constructor(_maxSettledAgeMs = 3e5) {
1425
+ this._maxSettledAgeMs = _maxSettledAgeMs;
1426
+ }
1427
+ /**
1428
+ * Register a freshly parked task. Assigns a task id and wires up settlement
1429
+ * bookkeeping. `onComplete` fires with the result when the task completes
1430
+ * (used to thread `lastResultVal`).
1431
+ */
1432
+ register(endpoint, debugName, task, onComplete) {
1433
+ this._prune();
1434
+ const id = `t${++this._seq}`;
1435
+ const entry = {
1436
+ id,
1437
+ debugName,
1438
+ endpoint,
1439
+ startedAt: Date.now(),
1440
+ task,
1441
+ outcome: void 0
1442
+ };
1443
+ this._byId.set(id, entry);
1444
+ this._live = entry;
1445
+ task.done.then((o) => {
1446
+ entry.outcome = o;
1447
+ if (this._live === entry) this._live = void 0;
1448
+ if (o.status === "completed") onComplete?.(o.result);
1449
+ }, () => {});
1450
+ return id;
1451
+ }
1452
+ /**
1453
+ * Cancel the single live task, if any (the supersede path). Resolves once
1454
+ * the cancellation settles.
1455
+ */
1456
+ async cancelLive() {
1457
+ const entry = this._live;
1458
+ if (!entry) return void 0;
1459
+ this._live = void 0;
1460
+ await entry.task.cancel();
1461
+ return {
1462
+ taskId: entry.id,
1463
+ debugName: entry.debugName,
1464
+ outcome: "cancelled"
1465
+ };
1466
+ }
1467
+ /**
1468
+ * Wait up to `timeoutMs` for `taskId` to settle. Returns `running` (with a
1469
+ * progress snapshot) if it is still going, otherwise its terminal result.
1470
+ */
1471
+ async awaitTask(taskId, timeoutMs) {
1472
+ const entry = this._byId.get(taskId);
1473
+ if (!entry) return {
1474
+ status: "unknown",
1475
+ taskId
1476
+ };
1477
+ if (!entry.outcome) {
1478
+ const timeout = new Promise((r) => {
1479
+ setTimeout(() => r("timeout"), timeoutMs).unref?.();
1480
+ });
1481
+ if (await Promise.race([entry.task.done, timeout]) === "timeout") return {
1482
+ status: "running",
1483
+ taskId,
1484
+ debugName: entry.debugName,
1485
+ inFlight: entry.task.inFlight(),
1486
+ logs: [...entry.task.logs]
1487
+ };
1488
+ }
1489
+ return this._outcomeResult(entry);
1490
+ }
1491
+ /** Explicitly cancel a task by id. */
1492
+ async cancel(taskId) {
1493
+ const entry = this._byId.get(taskId);
1494
+ if (!entry) return {
1495
+ status: "unknown",
1496
+ taskId
1497
+ };
1498
+ await entry.task.cancel();
1499
+ return this._outcomeResult(entry);
1500
+ }
1501
+ /** List live (unsettled) tasks. At most one under the global invariant. */
1502
+ list() {
1503
+ const out = [];
1504
+ for (const e of this._byId.values()) {
1505
+ if (e.outcome) continue;
1506
+ out.push({
1507
+ taskId: e.id,
1508
+ debugName: e.debugName,
1509
+ endpoint: e.endpoint,
1510
+ ageMs: Date.now() - e.startedAt,
1511
+ inFlight: e.task.inFlight(),
1512
+ logCount: e.task.logs.length
1513
+ });
1514
+ }
1515
+ return out;
1516
+ }
1517
+ /** Cancel every live task. */
1518
+ dispose() {
1519
+ for (const e of this._byId.values()) if (!e.outcome) e.task.cancel();
1520
+ this._byId.clear();
1521
+ this._live = void 0;
1522
+ }
1523
+ _outcomeResult(entry) {
1524
+ const o = entry.outcome;
1525
+ if (!o) return {
1526
+ status: "running",
1527
+ taskId: entry.id,
1528
+ debugName: entry.debugName,
1529
+ inFlight: entry.task.inFlight(),
1530
+ logs: [...entry.task.logs]
1531
+ };
1532
+ if (o.status === "completed") return {
1533
+ status: "completed",
1534
+ taskId: entry.id,
1535
+ debugName: entry.debugName,
1536
+ result: o.result,
1537
+ logs: o.logs
1538
+ };
1539
+ if (o.status === "cancelled") return {
1540
+ status: "cancelled",
1541
+ taskId: entry.id,
1542
+ debugName: entry.debugName,
1543
+ logs: o.logs
1544
+ };
1545
+ return {
1546
+ status: "error",
1547
+ taskId: entry.id,
1548
+ debugName: entry.debugName,
1549
+ error: o.error,
1550
+ logs: o.logs
1551
+ };
1552
+ }
1553
+ _prune() {
1554
+ const cutoff = Date.now() - this._maxSettledAgeMs;
1555
+ for (const [id, e] of this._byId) if (e.outcome && e.startedAt < cutoff) this._byId.delete(id);
1556
+ }
1557
+ };
1558
+ //#endregion
1559
+ //#region src/explore.ts
1560
+ const DEFAULT_LIMIT = 20;
1561
+ const MAX_LIMIT = 100;
1562
+ const MAX_MATCHES_PER_DOCUMENT = 20;
1563
+ const interfaceCache = /* @__PURE__ */ new WeakMap();
1564
+ /**
1565
+ * Browse the reflected directory, grep generated interface source, or inspect
1566
+ * one exact virtual document. Generated source is cached by the directory
1567
+ * route and interface hash; every call still walks the live directory.
1568
+ */
1569
+ async function explore(channel, args, deps) {
1570
+ if (args === void 0 || typeof args !== "object" || !("kind" in args)) throw new Error("explore requires `kind: \"browse\" | \"grep\" | \"inspect\"`.");
1571
+ const unlockGatedDirectory = args.requestPermission === true && deps !== void 0 ? (_serviceId) => deps.requestReflectionAccess() : void 0;
1572
+ const walked = await walkHubDetailed(channel, { unlockGatedDirectory });
1573
+ const listings = _filterAndSort(walked.listings, args);
1574
+ const inaccessible = _inaccessible(walked.inaccessible);
1575
+ switch (args.kind) {
1576
+ case "browse": {
1577
+ const page = _page(listings, args.limit, args.cursor);
1578
+ return {
1579
+ kind: "browse",
1580
+ total: listings.length,
1581
+ entries: page.items.map(_toListing),
1582
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {},
1583
+ ...inaccessible !== void 0 ? { inaccessible } : {}
1584
+ };
1585
+ }
1586
+ case "grep": {
1587
+ if (typeof args.pattern !== "string" || args.pattern.length === 0) throw new Error("explore grep requires a non-empty `pattern`.");
1588
+ const syntax = args.syntax ?? "regex";
1589
+ const matchesLine = _createLineMatcher(args.pattern, syntax);
1590
+ const contextLines = _boundedInteger(args.contextLines ?? 1, 0, 5, "contextLines");
1591
+ const loaded = await Promise.all(listings.map(async (listing) => {
1592
+ try {
1593
+ return { document: await _loadDocument(channel, listing) };
1594
+ } catch (error) {
1595
+ return { error: {
1596
+ serviceId: listing.serviceId,
1597
+ interfaceId: listing.interfaceId,
1598
+ error: error.message
1599
+ } };
1600
+ }
1601
+ }));
1602
+ const entries = [];
1603
+ const documentErrors = [];
1604
+ for (const item of loaded) {
1605
+ if (item.error !== void 0) {
1606
+ documentErrors.push(item.error);
1607
+ continue;
1608
+ }
1609
+ const document = item.document;
1610
+ const matches = _grepDocument(document, matchesLine, contextLines);
1611
+ if (matches.length === 0) continue;
1612
+ entries.push({
1613
+ ..._toListing(document.listing),
1614
+ matches: matches.slice(0, MAX_MATCHES_PER_DOCUMENT),
1615
+ ...matches.length > MAX_MATCHES_PER_DOCUMENT ? { matchesTruncated: true } : {}
1616
+ });
1617
+ }
1618
+ const page = _page(entries, args.limit, args.cursor);
1619
+ return {
1620
+ kind: "grep",
1621
+ pattern: args.pattern,
1622
+ syntax,
1623
+ total: entries.length,
1624
+ entries: page.items,
1625
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {},
1626
+ ...documentErrors.length > 0 ? { documentErrors } : {},
1627
+ ...inaccessible !== void 0 ? { inaccessible } : {}
1628
+ };
1629
+ }
1630
+ case "inspect": {
1631
+ const listing = listings.find((item) => item.serviceId === args.serviceId && item.interfaceId === args.interfaceId);
1632
+ if (listing === void 0) throw new Error(`Interface not found: ${args.serviceId}::${args.interfaceId}. Browse first, or set \`requestPermission: true\` if its directory is gated.`);
1633
+ const document = await _loadDocument(channel, listing);
1634
+ const format = args.format ?? "source";
1635
+ return {
1636
+ kind: "inspect",
1637
+ format,
1638
+ ..._toListing(listing),
1639
+ ...format === "source" ? { source: document.source } : { schema: document.schema },
1640
+ ...inaccessible !== void 0 ? { inaccessible } : {}
1641
+ };
1642
+ }
1643
+ default: throw new Error(`Unknown explore kind: ${String(args.kind)}`);
1644
+ }
1645
+ }
1646
+ function _filterAndSort(listings, args) {
1647
+ const includeInternal = args.includeInternal === true || args.interfaceId !== void 0 && _isHubrpcInternalInterface(args.interfaceId);
1648
+ return listings.filter((listing) => {
1649
+ if (args.serviceId !== void 0 && listing.serviceId !== args.serviceId) return false;
1650
+ if (args.interfaceId !== void 0 && listing.interfaceId !== args.interfaceId) return false;
1651
+ return includeInternal || !_isHubrpcInternalInterface(listing.interfaceId);
1652
+ }).sort((a, b) => a.serviceId.localeCompare(b.serviceId) || a.interfaceId.localeCompare(b.interfaceId) || a.hash.localeCompare(b.hash));
1653
+ }
1654
+ function _toListing(listing) {
1655
+ return {
1656
+ serviceId: listing.serviceId,
1657
+ ...listing.serviceDescription !== void 0 ? { serviceDescription: listing.serviceDescription } : {},
1658
+ interfaceId: listing.interfaceId,
1659
+ interfaceHash: listing.hash,
1660
+ documentId: _documentId(listing)
1661
+ };
1662
+ }
1663
+ async function _loadDocument(channel, listing) {
1664
+ let cache = interfaceCache.get(channel);
1665
+ if (cache === void 0) {
1666
+ cache = /* @__PURE__ */ new Map();
1667
+ interfaceCache.set(channel, cache);
1668
+ }
1669
+ const cacheKey = `${listing.discoveredFrom}\u0000${listing.interfaceId}\u0000${listing.hash}`;
1670
+ let pending = cache.get(cacheKey);
1671
+ if (pending === void 0) {
1672
+ pending = (async () => {
1673
+ const target = listing.discoveredFrom || listing.serviceId || void 0;
1674
+ const schema = await fetchSchema(channel, listing.interfaceId, listing.hash, target);
1675
+ return {
1676
+ schema,
1677
+ generatedSource: generateTsInterface(schema)
1678
+ };
1679
+ })();
1680
+ cache.set(cacheKey, pending);
1681
+ }
1682
+ let cached;
1683
+ try {
1684
+ cached = await pending;
1685
+ } catch (error) {
1686
+ if (cache.get(cacheKey) === pending) cache.delete(cacheKey);
1687
+ throw error;
1688
+ }
1689
+ const documentId = _documentId(listing);
1690
+ return {
1691
+ ...cached,
1692
+ listing,
1693
+ source: `${_documentHeader(listing, documentId)}\n${cached.generatedSource}`
1694
+ };
1695
+ }
1696
+ function _documentHeader(listing, documentId) {
1697
+ return [
1698
+ `// virtualDocument: ${JSON.stringify(documentId)}`,
1699
+ `// serviceId: ${JSON.stringify(listing.serviceId)}`,
1700
+ ...listing.serviceDescription !== void 0 ? [`// serviceDescription: ${JSON.stringify(listing.serviceDescription)}`] : [],
1701
+ `// interfaceId: ${JSON.stringify(listing.interfaceId)}`,
1702
+ `// interfaceHash: ${JSON.stringify(listing.hash)}`,
1703
+ `// discoveredFrom: ${JSON.stringify(listing.discoveredFrom)}`
1704
+ ].join("\n");
1705
+ }
1706
+ function _documentId(listing) {
1707
+ return `linkrpc://${listing.serviceId === "" ? "$root" : encodeURIComponent(listing.serviceId)}/${encodeURIComponent(listing.interfaceId)}@${listing.hash}.ts`;
1708
+ }
1709
+ function _grepDocument(document, matchesLine, contextLines) {
1710
+ const lines = document.source.split("\n");
1711
+ const memberAtLine = _memberMap(lines, document.schema);
1712
+ const ranges = [];
1713
+ for (let index = 0; index < lines.length; index++) {
1714
+ if (!matchesLine(lines[index])) continue;
1715
+ const start = Math.max(0, index - contextLines);
1716
+ const end = Math.min(lines.length, index + contextLines + 1);
1717
+ const previous = ranges.at(-1);
1718
+ if (previous !== void 0 && start <= previous.end) {
1719
+ previous.end = Math.max(previous.end, end);
1720
+ previous.matches.push(index);
1721
+ } else ranges.push({
1722
+ start,
1723
+ end,
1724
+ matches: [index]
1725
+ });
1726
+ }
1727
+ return ranges.map((range) => {
1728
+ const members = new Set(range.matches.map((line) => memberAtLine[line]));
1729
+ const member = members.size === 1 ? members.values().next().value : void 0;
1730
+ return {
1731
+ searchResult: lines.slice(range.start, range.end).join("\n"),
1732
+ lineRange: [range.start + 1, range.end],
1733
+ ...member !== void 0 ? { member } : {}
1734
+ };
1735
+ });
1736
+ }
1737
+ function _memberMap(lines, schema) {
1738
+ const starts = [];
1739
+ let searchFrom = 0;
1740
+ for (const methodName of Object.keys(schema.methods)) {
1741
+ const barePrefix = `${methodName}: `;
1742
+ const quotedPrefix = `${JSON.stringify(methodName)}: `;
1743
+ const line = lines.findIndex((value, index) => index >= searchFrom && (value.trimStart().startsWith(barePrefix) || value.trimStart().startsWith(quotedPrefix)) && (value.includes("requestType(") || value.includes("notificationType(")));
1744
+ if (line >= 0) {
1745
+ starts.push({
1746
+ line,
1747
+ member: methodName
1748
+ });
1749
+ searchFrom = line + 1;
1750
+ }
1751
+ }
1752
+ const result = new Array(lines.length);
1753
+ for (let i = 0; i < starts.length; i++) {
1754
+ const end = starts[i + 1]?.line ?? lines.length;
1755
+ for (let line = starts[i].line; line < end; line++) result[line] = starts[i].member;
1756
+ }
1757
+ return result;
1758
+ }
1759
+ function _createLineMatcher(pattern, syntax) {
1760
+ if (syntax === "literal") {
1761
+ const needle = pattern.toLocaleLowerCase();
1762
+ return (line) => line.toLocaleLowerCase().includes(needle);
1763
+ }
1764
+ if (syntax !== "regex") throw new Error(`Unknown grep syntax: ${String(syntax)}`);
1765
+ let regex;
1766
+ try {
1767
+ regex = new RegExp(pattern, "iu");
1768
+ } catch (error) {
1769
+ throw new Error(`Invalid grep regular expression: ${error.message}`);
1770
+ }
1771
+ return (line) => regex.test(line);
1772
+ }
1773
+ function _page(items, requestedLimit, cursor) {
1774
+ const limit = _boundedInteger(requestedLimit ?? DEFAULT_LIMIT, 1, MAX_LIMIT, "limit");
1775
+ const offset = _decodeCursor(cursor);
1776
+ const page = items.slice(offset, offset + limit);
1777
+ const nextOffset = offset + page.length;
1778
+ return {
1779
+ items: page,
1780
+ ...nextOffset < items.length ? { nextCursor: String(nextOffset) } : {}
1781
+ };
1782
+ }
1783
+ function _decodeCursor(cursor) {
1784
+ if (cursor === void 0) return 0;
1785
+ if (!/^\d+$/.test(cursor)) throw new Error("Invalid explore cursor.");
1786
+ return Number(cursor);
1787
+ }
1788
+ function _boundedInteger(value, min, max, name) {
1789
+ if (!Number.isInteger(value) || value < min || value > max) throw new Error(`explore ${name} must be an integer from ${min} to ${max}.`);
1790
+ return value;
1791
+ }
1792
+ function _inaccessible(directories) {
1793
+ if (directories.length === 0) return void 0;
1794
+ return directories.map((directory) => ({
1795
+ serviceId: directory.serviceId,
1796
+ reason: directory.reason,
1797
+ hint: "Repeat this explore call with `requestPermission: true` to search all gated directories."
1798
+ }));
1799
+ }
1800
+ function _isHubrpcInternalInterface(interfaceId) {
1801
+ return interfaceId.startsWith("hubrpc.");
1802
+ }
1803
+ //#endregion
1804
+ //#region src/resultPresentation.ts
1805
+ const MCP_PRESENTATION_TAG = "__linkrpcMcpPresentationV1";
1806
+ /**
1807
+ * Convert a JSON tool-result envelope into MCP-native content while preserving a
1808
+ * JSON text fallback for clients that only consume text.
1809
+ */
1810
+ function presentToolResult(envelope, mode = "auto") {
1811
+ if (mode === "raw") {
1812
+ const rawEnvelope = stripPresentationTags(envelope);
1813
+ const resultOptions = findExplicitResultOptions(envelope);
1814
+ return {
1815
+ content: [{
1816
+ type: "text",
1817
+ text: JSON.stringify(rawEnvelope, null, 2)
1818
+ }],
1819
+ structuredContent: rawEnvelope,
1820
+ ...resultOptions
1821
+ };
1822
+ }
1823
+ const state = { content: [] };
1824
+ const structuredContent = presentValue(envelope, "$", state);
1825
+ return {
1826
+ content: [{
1827
+ type: "text",
1828
+ text: JSON.stringify(structuredContent, null, 2)
1829
+ }, ...state.content],
1830
+ structuredContent,
1831
+ ...state.isError !== void 0 ? { isError: state.isError } : {},
1832
+ ...state.meta ? { _meta: state.meta } : {}
1833
+ };
1834
+ }
1835
+ /** Remove presentation-only wrappers before persisting a script result. */
1836
+ function scriptResultValue(value) {
1837
+ return stripPresentationTags(value);
1838
+ }
1839
+ function presentValue(value, path, state) {
1840
+ const tag = getPresentationTag(value);
1841
+ if (tag?.kind === "raw") return stripPresentationTags(tag.value);
1842
+ if (tag?.kind === "result") {
1843
+ if (tag.isError !== void 0) {
1844
+ if (typeof tag.isError !== "boolean") throw new Error(`Invalid MCP result isError at ${path}: expected boolean`);
1845
+ state.isError = tag.isError;
1846
+ }
1847
+ if (tag._meta !== void 0) {
1848
+ if (!isRecord(tag._meta)) throw new Error(`Invalid MCP result _meta at ${path}: expected object`);
1849
+ state.meta = tag._meta;
1850
+ }
1851
+ const presented = presentValue(Object.hasOwn(tag, "structuredContent") ? tag.structuredContent : Object.hasOwn(tag, "value") ? tag.value : null, path, state);
1852
+ const content = Array.isArray(tag.content) ? tag.content : [];
1853
+ for (let i = 0; i < content.length; i++) addExplicitContent(content[i], `${path}.content[${i}]`, state);
1854
+ return presented;
1855
+ }
1856
+ if (tag?.kind === "content") {
1857
+ const block = parseExplicitContent(tag.content, path);
1858
+ state.content.push(block);
1859
+ return describeContent(block, path);
1860
+ }
1861
+ const explicitContent = ContentBlockSchema.safeParse(value);
1862
+ if (explicitContent.success) {
1863
+ state.content.push(explicitContent.data);
1864
+ return describeContentWithMetadata(explicitContent.data, contentBlockMetadata(value, explicitContent.data), path, state);
1865
+ }
1866
+ const automaticContent = detectAutomaticContent(value);
1867
+ if (automaticContent) {
1868
+ state.content.push(automaticContent.block);
1869
+ return describeContentWithMetadata(automaticContent.block, automaticContent.metadata, path, state);
1870
+ }
1871
+ if (Array.isArray(value)) return value.map((item, index) => presentValue(item, `${path}[${index}]`, state));
1872
+ if (isRecord(value)) {
1873
+ const result = {};
1874
+ for (const [key, item] of Object.entries(value)) result[key] = presentValue(item, `${path}.${key}`, state);
1875
+ return result;
1876
+ }
1877
+ return value;
1878
+ }
1879
+ function addExplicitContent(value, path, state) {
1880
+ const tag = getPresentationTag(value);
1881
+ state.content.push(parseExplicitContent(tag?.kind === "content" ? tag.content : value, path));
1882
+ }
1883
+ function parseExplicitContent(value, path) {
1884
+ const parsed = ContentBlockSchema.safeParse(value);
1885
+ if (parsed.success) return parsed.data;
1886
+ throw new Error(`Invalid MCP content block at ${path}: ${parsed.error.message}`);
1887
+ }
1888
+ function detectAutomaticContent(value) {
1889
+ if (typeof value === "string") {
1890
+ const dataUrl = parseDataUrl(value);
1891
+ if (dataUrl) return { block: blockForBinary(dataUrl.data, dataUrl.mimeType) };
1892
+ const base64 = normalizeBase64(value);
1893
+ if (!base64) return;
1894
+ const mimeType = sniffMimeType(base64);
1895
+ return mimeType ? { block: blockForBinary(base64, mimeType) } : void 0;
1896
+ }
1897
+ if (!isRecord(value) || typeof value.mimeType !== "string") return;
1898
+ if (typeof value.text === "string") return {
1899
+ block: {
1900
+ type: "resource",
1901
+ resource: {
1902
+ uri: typeof value.uri === "string" ? value.uri : contentUrn(value.mimeType, value.text),
1903
+ mimeType: value.mimeType,
1904
+ text: value.text
1905
+ }
1906
+ },
1907
+ metadata: contentMetadata(value)
1908
+ };
1909
+ const encoded = [
1910
+ value.data,
1911
+ value.base64,
1912
+ value.blob
1913
+ ].find((candidate) => typeof candidate === "string");
1914
+ if (!encoded) return;
1915
+ const dataUrl = parseDataUrl(encoded);
1916
+ const data = dataUrl?.data ?? normalizeBase64(encoded);
1917
+ if (!data) return;
1918
+ return {
1919
+ block: blockForBinary(data, dataUrl?.mimeType ?? value.mimeType, typeof value.uri === "string" ? value.uri : void 0),
1920
+ metadata: contentMetadata(value)
1921
+ };
1922
+ }
1923
+ function blockForBinary(data, mimeType, uri) {
1924
+ if (mimeType.startsWith("image/")) return {
1925
+ type: "image",
1926
+ data,
1927
+ mimeType
1928
+ };
1929
+ if (mimeType.startsWith("audio/")) return {
1930
+ type: "audio",
1931
+ data,
1932
+ mimeType
1933
+ };
1934
+ return {
1935
+ type: "resource",
1936
+ resource: {
1937
+ uri: uri ?? contentUrn(mimeType, data),
1938
+ mimeType,
1939
+ blob: data
1940
+ }
1941
+ };
1942
+ }
1943
+ function describeContent(content, path) {
1944
+ switch (content.type) {
1945
+ case "text": return content.text;
1946
+ case "image":
1947
+ case "audio": return { $content: {
1948
+ type: content.type,
1949
+ mimeType: content.mimeType,
1950
+ bytes: base64ByteLength(content.data),
1951
+ path
1952
+ } };
1953
+ case "resource_link": return { $content: {
1954
+ type: content.type,
1955
+ uri: content.uri,
1956
+ name: content.name,
1957
+ ...content.mimeType ? { mimeType: content.mimeType } : {},
1958
+ ...content.size !== void 0 ? { size: content.size } : {},
1959
+ path
1960
+ } };
1961
+ case "resource": {
1962
+ const resource = content.resource;
1963
+ return { $content: {
1964
+ type: content.type,
1965
+ uri: resource.uri,
1966
+ ...resource.mimeType ? { mimeType: resource.mimeType } : {},
1967
+ ..."blob" in resource ? { bytes: base64ByteLength(resource.blob) } : { characters: resource.text.length },
1968
+ path
1969
+ } };
1970
+ }
1971
+ }
1972
+ }
1973
+ function describeContentWithMetadata(content, metadata, path, state) {
1974
+ const description = describeContent(content, path);
1975
+ if (!metadata || Object.keys(metadata).length === 0) return description;
1976
+ const presentedMetadata = presentValue(metadata, path, state);
1977
+ if (isRecord(description)) return {
1978
+ ...presentedMetadata,
1979
+ ...description
1980
+ };
1981
+ return {
1982
+ ...presentedMetadata,
1983
+ $content: {
1984
+ type: content.type,
1985
+ characters: typeof description === "string" ? description.length : void 0,
1986
+ path
1987
+ }
1988
+ };
1989
+ }
1990
+ function stripPresentationTags(value) {
1991
+ const tag = getPresentationTag(value);
1992
+ if (tag?.kind === "raw") return stripPresentationTags(tag.value);
1993
+ if (tag?.kind === "result") {
1994
+ if (Object.hasOwn(tag, "value")) return stripPresentationTags(tag.value);
1995
+ if (Object.hasOwn(tag, "structuredContent")) return stripPresentationTags(tag.structuredContent);
1996
+ return null;
1997
+ }
1998
+ if (tag?.kind === "content") return stripPresentationTags(tag.content);
1999
+ if (Array.isArray(value)) return value.map(stripPresentationTags);
2000
+ if (isRecord(value)) {
2001
+ const result = {};
2002
+ for (const [key, item] of Object.entries(value)) result[key] = stripPresentationTags(item);
2003
+ return result;
2004
+ }
2005
+ return value;
2006
+ }
2007
+ function findExplicitResultOptions(value) {
2008
+ const tag = getPresentationTag(value);
2009
+ if (tag?.kind === "raw") return {};
2010
+ if (tag?.kind === "result") return {
2011
+ ...typeof tag.isError === "boolean" ? { isError: tag.isError } : {},
2012
+ ...isRecord(tag._meta) ? { _meta: tag._meta } : {}
2013
+ };
2014
+ if (Array.isArray(value)) for (const item of value) {
2015
+ const options = findExplicitResultOptions(item);
2016
+ if (options.isError !== void 0 || options._meta !== void 0) return options;
2017
+ }
2018
+ else if (isRecord(value)) for (const item of Object.values(value)) {
2019
+ const options = findExplicitResultOptions(item);
2020
+ if (options.isError !== void 0 || options._meta !== void 0) return options;
2021
+ }
2022
+ return {};
2023
+ }
2024
+ function getPresentationTag(value) {
2025
+ if (!isRecord(value) || Object.keys(value).length !== 1) return;
2026
+ const candidate = value[MCP_PRESENTATION_TAG];
2027
+ if (!isRecord(candidate) || candidate.kind !== "raw" && candidate.kind !== "result" && candidate.kind !== "content") return;
2028
+ return candidate;
2029
+ }
2030
+ function parseDataUrl(value) {
2031
+ if (!value.startsWith("data:")) return;
2032
+ const comma = value.indexOf(",");
2033
+ if (comma < 5) return;
2034
+ const parts = value.slice(5, comma).split(";");
2035
+ const mimeType = parts[0] || "text/plain";
2036
+ const payload = value.slice(comma + 1);
2037
+ if (parts.includes("base64")) {
2038
+ const data = normalizeBase64(payload);
2039
+ return data ? {
2040
+ data,
2041
+ mimeType
2042
+ } : void 0;
2043
+ }
2044
+ try {
2045
+ return {
2046
+ data: Buffer.from(decodeURIComponent(payload), "utf8").toString("base64"),
2047
+ mimeType
2048
+ };
2049
+ } catch {
2050
+ return;
2051
+ }
2052
+ }
2053
+ function normalizeBase64(value) {
2054
+ const compact = value.replace(/\s/g, "");
2055
+ if (compact.length < 8 || compact.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return;
2056
+ const withoutPadding = compact.replace(/=+$/, "");
2057
+ const padding = (4 - withoutPadding.length % 4) % 4;
2058
+ return withoutPadding + "=".repeat(padding);
2059
+ }
2060
+ function sniffMimeType(base64) {
2061
+ const bytes = Buffer.from(base64.slice(0, 256), "base64");
2062
+ const ascii = bytes.toString("ascii");
2063
+ if (startsWith(bytes, [
2064
+ 137,
2065
+ 80,
2066
+ 78,
2067
+ 71,
2068
+ 13,
2069
+ 10,
2070
+ 26,
2071
+ 10
2072
+ ])) return "image/png";
2073
+ if (startsWith(bytes, [
2074
+ 255,
2075
+ 216,
2076
+ 255
2077
+ ])) return "image/jpeg";
2078
+ if (ascii.startsWith("GIF87a") || ascii.startsWith("GIF89a")) return "image/gif";
2079
+ if (ascii.startsWith("RIFF") && ascii.slice(8, 12) === "WEBP") return "image/webp";
2080
+ if (ascii.startsWith("BM")) return "image/bmp";
2081
+ if (startsWith(bytes, [
2082
+ 0,
2083
+ 0,
2084
+ 1,
2085
+ 0
2086
+ ])) return "image/x-icon";
2087
+ if (startsWith(bytes, [
2088
+ 73,
2089
+ 73,
2090
+ 42,
2091
+ 0
2092
+ ]) || startsWith(bytes, [
2093
+ 77,
2094
+ 77,
2095
+ 0,
2096
+ 42
2097
+ ])) return "image/tiff";
2098
+ if (/^\s*(?:<\?xml[^>]*>\s*)?<svg[\s>]/i.test(bytes.toString("utf8"))) return "image/svg+xml";
2099
+ if (ascii.startsWith("RIFF") && ascii.slice(8, 12) === "WAVE") return "audio/wav";
2100
+ if (ascii.startsWith("ID3") || startsWith(bytes, [255, 251]) || startsWith(bytes, [255, 243]) || startsWith(bytes, [255, 242])) return "audio/mpeg";
2101
+ if (ascii.startsWith("OggS")) return "audio/ogg";
2102
+ if (ascii.startsWith("fLaC")) return "audio/flac";
2103
+ if (startsWith(bytes, [255, 241]) || startsWith(bytes, [255, 249])) return "audio/aac";
2104
+ if (ascii.startsWith("MThd")) return "audio/midi";
2105
+ if (ascii.startsWith("%PDF-")) return "application/pdf";
2106
+ if (startsWith(bytes, [
2107
+ 80,
2108
+ 75,
2109
+ 3,
2110
+ 4
2111
+ ])) return "application/zip";
2112
+ if (startsWith(bytes, [31, 139])) return "application/gzip";
2113
+ }
2114
+ function startsWith(value, prefix) {
2115
+ return prefix.every((byte, index) => value[index] === byte);
2116
+ }
2117
+ function base64ByteLength(value) {
2118
+ const normalized = normalizeBase64(value);
2119
+ if (!normalized) return 0;
2120
+ const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
2121
+ return normalized.length / 4 * 3 - padding;
2122
+ }
2123
+ function contentUrn(mimeType, value) {
2124
+ return `urn:linkrpc-mcp:content:${createHash("sha256").update(mimeType).update("\0").update(value).digest("base64url")}`;
2125
+ }
2126
+ function contentMetadata(value) {
2127
+ const metadata = {};
2128
+ for (const [key, item] of Object.entries(value)) if (![
2129
+ "data",
2130
+ "base64",
2131
+ "blob",
2132
+ "text",
2133
+ "mimeType",
2134
+ "uri"
2135
+ ].includes(key)) metadata[key] = item;
2136
+ return metadata;
2137
+ }
2138
+ function contentBlockMetadata(value, content) {
2139
+ const standardKeys = content.type === "text" ? [
2140
+ "type",
2141
+ "text",
2142
+ "annotations",
2143
+ "_meta"
2144
+ ] : content.type === "image" || content.type === "audio" ? [
2145
+ "type",
2146
+ "data",
2147
+ "mimeType",
2148
+ "annotations",
2149
+ "_meta"
2150
+ ] : content.type === "resource" ? [
2151
+ "type",
2152
+ "resource",
2153
+ "annotations",
2154
+ "_meta"
2155
+ ] : [
2156
+ "type",
2157
+ "uri",
2158
+ "name",
2159
+ "title",
2160
+ "description",
2161
+ "mimeType",
2162
+ "size",
2163
+ "annotations",
2164
+ "_meta",
2165
+ "icons"
2166
+ ];
2167
+ const metadata = {};
2168
+ for (const [key, item] of Object.entries(value)) if (!standardKeys.includes(key)) metadata[key] = item;
2169
+ return metadata;
2170
+ }
2171
+ function isRecord(value) {
2172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2173
+ }
2174
+ //#endregion
2175
+ //#region src/server.ts
2176
+ const CONNECTION_SCHEMA = z.string().optional().describe("Endpoint URI of the hub to connect to, e.g. `unix:/run/hub.sock?token=…`, `npipe://./pipe/vscode-linkrpc-…?token=…`, or `wss://host:port?token=…`. The token may be embedded as a `token` query param or supplied via the LINKRPC_TOKEN environment variable. When omitted entirely, the server falls back to the LINKRPC_ENDPOINT and LINKRPC_TOKEN environment variables.");
2177
+ const PRESENTATION_SCHEMA = z.enum(["auto", "raw"]).optional().describe("How to present the returned value. `auto` (default) recognizes MCP content blocks, data URLs, common base64 media signatures, and `{ data|base64|blob, mimeType }` objects, emitting native image/audio/resource content while keeping the original value available to subsequent scripts through `lastResultVal`. `raw` disables transformation and exposes the original JSON/base64.");
2178
+ const SERVER_NAME = "linkrpc-mcp";
2179
+ const SERVER_VERSION = "0.0.1";
2180
+ /**
2181
+ * MCP server exposing the `runLinkRpcScript` tool plus task-management helpers.
2182
+ *
2183
+ * Documentation for the sandbox API (`con`) is reachable from inside the
2184
+ * sandbox via `con.getDocs()` rather than an MCP resource — that way the
2185
+ * model can pull it in by issuing a one-line `runLinkRpcScript` call.
2186
+ *
2187
+ * Can be hosted over stdio (CLI mode, via {@link startStdio}) or over any
2188
+ * MCP {@link Transport} (e.g. Streamable HTTP, used by the VS Code Team
2189
+ * Tools extension which embeds this server in-process).
2190
+ */
2191
+ var LinkRpcMcpServer = class LinkRpcMcpServer {
2192
+ static async startStdio(options) {
2193
+ const server = new LinkRpcMcpServer(options);
2194
+ await server.connect(new StdioServerTransport());
2195
+ return server;
2196
+ }
2197
+ _mcp;
2198
+ _pool;
2199
+ _tasks = new TaskRegistry();
2200
+ _onToolCall;
2201
+ _onExploreCall;
2202
+ constructor(options = {}) {
2203
+ this._mcp = new McpServer({
2204
+ name: SERVER_NAME,
2205
+ version: SERVER_VERSION
2206
+ });
2207
+ this._onToolCall = options.onToolCall;
2208
+ this._onExploreCall = options.onExploreCall;
2209
+ this._pool = options.pool ?? (options.provider ? new ProviderPool(options.provider) : new ConnectionPool({
2210
+ defaultEndpoint: options.defaultEndpoint,
2211
+ ...options.defaultConnection ? { defaultTransport: options.defaultConnection } : {}
2212
+ }));
2213
+ this._registerTools();
2214
+ }
2215
+ async connect(transport) {
2216
+ await this._mcp.connect(transport);
2217
+ }
2218
+ dispose() {
2219
+ this._tasks.dispose();
2220
+ this._pool.dispose();
2221
+ }
2222
+ _registerTools() {
2223
+ this._mcp.registerTool("runLinkRpcScript", {
2224
+ title: "Run JS against a linkrpc hub",
2225
+ description: "Evaluate a JS function inside a QuickJS sandbox with `con` (the live hub connection), `lastResultVal` (previous call's original result), and `mcp` (optional result-presentation helpers) in scope.\n\nBefore composing non-trivial calls, fetch the full sandbox API docs by running:\n `async ({ con }) => con.getDocs()`\nThat returns the TypeScript declarations for the full script context, including `con`, `mcp`, sandbox limits, and available console.\n\n`code` MUST be a function expression, e.g.\n `({ con }) => con.call(\"vscode\", \"vscode.window\", \"showInformationMessage\", { message: \"hi\" })`\n\nSet `connection` to a hub endpoint URI (e.g. `unix:/path?token=…` or `wss://host?token=…`) to target a specific hub, or leave it out to use the LINKRPC_ENDPOINT / LINKRPC_TOKEN env vars.\n\nACCESS IS NOT AUTOMATIC: a gated `con.call(...)` is NOT silently granted. Reflection (`con.explore`) works out of the box, but calling a service member you have no capability for fails with a permission error. Inspect what you already hold with `con.grants()`, then ask the user for access — ideally for MANY members at once — with `con.requestAccess({ permissions, duration })`. Pick `duration`: `once` for a one-off, `session` for the rest of this connection, `persistent` to remember across runs. Batch related permissions into a single request so the user sees one prompt instead of many.\n\nBACKGROUND TASKS: if the code is still awaiting I/O (an RPC reply, a user dialog, a stream) when `foregroundMs` elapses, the call does NOT fail — it is parked as a background task and the result is `{ status: \"running\", taskId, debugName, inFlight }`. Use `awaitLinkRpcTask` to wait for it or `cancelLinkRpcTask` to abandon it. Set `label` to give the task a clear debug name. Starting a new `runLinkRpcScript` cancels any task still parked (reported as `supersededTask`).\n\nRESULT PRESENTATION: returned image/audio/resource data is automatically emitted as native MCP content when it is a data URL, recognizable base64, an MCP content block, or an object shaped like `{ data|base64|blob, mimeType }`. This happens only after the script finishes: values passed between hub calls and saved in `lastResultVal` remain original and unmodified. Use `presentation: \"raw\"` or `mcp.raw(value)` when the model needs the literal base64. Use `mcp.image`, `mcp.audio`, `mcp.resource`, `mcp.resourceLink`, or `mcp.result` for explicit control. Native content always includes JSON text and structured fallbacks.\n\nThe result contains `{ status, result | taskId, logs, endpoint }`. Pass `trace: true` to also receive a per-call JSON-RPC wire log under `trace` — useful when a call fails or returns unexpected data.",
2226
+ inputSchema: {
2227
+ connection: CONNECTION_SCHEMA,
2228
+ code: z.string().describe("JS function expression. Receives `{ con, lastResultVal, mcp }`. Sync or async. Return value is JSON-stringified."),
2229
+ presentation: PRESENTATION_SCHEMA,
2230
+ label: z.string().optional().describe("Human-readable debug name for the task if it gets parked, e.g. \"ask user to confirm deploy\". When omitted, the name is inferred from the in-flight RPC(s) at park time."),
2231
+ foregroundMs: z.number().int().positive().optional().describe("How long the call runs synchronously before being parked as a background task. Defaults to 5000."),
2232
+ maxLifetimeMs: z.number().int().positive().optional().describe("Absolute lifetime cap for a parked task before it is cancelled. Defaults to 120000."),
2233
+ trace: z.boolean().optional().describe("When true, include a `trace` array in the result with every outbound JSON-RPC request / notification, its outcome, and every permission round-trip. Off by default to keep results small.")
2234
+ },
2235
+ annotations: { readOnlyHint: true }
2236
+ }, async (args) => this._observeToolCall("runLinkRpcScript", args, async () => {
2237
+ let pooled;
2238
+ try {
2239
+ pooled = await this._pool.resolve(args.connection);
2240
+ } catch (e) {
2241
+ return _errorResult(_describeError(e));
2242
+ }
2243
+ const superseded = await this._tasks.cancelLive();
2244
+ const traceEnabled = args.trace === true;
2245
+ const trace = [];
2246
+ const disposeTrace = traceEnabled ? pooled.addTraceListener((line) => trace.push(line)) : () => {};
2247
+ const host = {
2248
+ call: async (method, paramsJson, optsJson, onStreamMessage, signal) => {
2249
+ const params = paramsJson === "" ? {} : JSON.parse(paramsJson);
2250
+ const requestPermission = (optsJson === "" ? {} : JSON.parse(optsJson)).requestPermission === true;
2251
+ const send = async () => {
2252
+ if (signal.aborted) {
2253
+ const error = /* @__PURE__ */ new Error(`AbortError: ${String(signal.reason ?? "cancelled")}`);
2254
+ error.name = "AbortError";
2255
+ throw error;
2256
+ }
2257
+ const call = pooled.channel.sendRequestWithStream(method, params, { onStreamMessage: (payload) => onStreamMessage(JSON.stringify(payload)) });
2258
+ const cancel = () => {
2259
+ const reason = String(signal.reason ?? "cancelled");
2260
+ call.cancel(reason);
2261
+ call.dispose?.(reason);
2262
+ };
2263
+ signal.addEventListener("abort", cancel, { once: true });
2264
+ try {
2265
+ return await call.result;
2266
+ } finally {
2267
+ signal.removeEventListener("abort", cancel);
2268
+ }
2269
+ };
2270
+ try {
2271
+ const result = await send();
2272
+ return JSON.stringify(result ?? null);
2273
+ } catch (e) {
2274
+ if (requestPermission && _isPermissionError(e)) {
2275
+ if (await _autoRequestAccess(pooled.session, method)) {
2276
+ const result = await send();
2277
+ return JSON.stringify(result ?? null);
2278
+ }
2279
+ }
2280
+ throw _enrichPermissionError(e, method);
2281
+ }
2282
+ },
2283
+ notify: async (method, paramsJson) => {
2284
+ const params = paramsJson === "" ? {} : JSON.parse(paramsJson);
2285
+ await pooled.channel.sendNotification(method, params);
2286
+ return "";
2287
+ },
2288
+ explore: async (argsJson) => {
2289
+ const exploreArgs = argsJson === "" ? {} : JSON.parse(argsJson);
2290
+ let reflectionGrant;
2291
+ try {
2292
+ const result = await explore(pooled.channel, exploreArgs, { requestReflectionAccess: () => reflectionGrant ??= _requestReflectionAccess(pooled.session) });
2293
+ this._observeExploreCall({
2294
+ arguments: exploreArgs,
2295
+ result
2296
+ });
2297
+ return JSON.stringify(result);
2298
+ } catch (error) {
2299
+ this._observeExploreCall({
2300
+ arguments: exploreArgs,
2301
+ error: error instanceof Error ? error.message : String(error)
2302
+ });
2303
+ throw error;
2304
+ }
2305
+ },
2306
+ requestAccess: async (argsJson) => {
2307
+ const req = argsJson === "" ? {} : JSON.parse(argsJson);
2308
+ const result = await pooled.session.requestAccess({
2309
+ consumer: {
2310
+ name: "linkrpc-mcp",
2311
+ purpose: req.purpose
2312
+ },
2313
+ permissions: req.permissions ?? [],
2314
+ duration: req.duration
2315
+ });
2316
+ return JSON.stringify(result);
2317
+ },
2318
+ grants: async () => JSON.stringify(summarizeGrants(pooled.session.listGrants()))
2319
+ };
2320
+ try {
2321
+ const outcome = await startSandbox(args.code, host, pooled.lastResultVal, {
2322
+ foregroundMs: args.foregroundMs,
2323
+ maxLifetimeMs: args.maxLifetimeMs,
2324
+ label: args.label
2325
+ });
2326
+ if (outcome.status === "completed") {
2327
+ pooled.lastResultVal = scriptResultValue(outcome.result);
2328
+ return presentToolResult({
2329
+ status: "completed",
2330
+ result: outcome.result,
2331
+ logs: outcome.logs,
2332
+ ...traceEnabled ? { trace } : {},
2333
+ endpoint: pooled.endpoint,
2334
+ ...superseded ? { supersededTask: superseded } : {}
2335
+ }, args.presentation);
2336
+ }
2337
+ if (outcome.status === "error") return _errorResult(`runLinkRpcScript failed: ${outcome.error}`, traceEnabled ? trace : void 0);
2338
+ return presentToolResult({
2339
+ status: "running",
2340
+ taskId: this._tasks.register(pooled.endpoint, outcome.debugName, outcome.task, (result) => {
2341
+ pooled.lastResultVal = scriptResultValue(result);
2342
+ }),
2343
+ debugName: outcome.debugName,
2344
+ inFlight: outcome.task.inFlight(),
2345
+ logsSoFar: outcome.task.logs,
2346
+ ...traceEnabled ? { traceSoFar: trace } : {},
2347
+ endpoint: pooled.endpoint,
2348
+ ...superseded ? { supersededTask: superseded } : {}
2349
+ }, "raw");
2350
+ } catch (e) {
2351
+ return _errorResult(`runLinkRpcScript failed: ${_describeError(e)}`, traceEnabled ? trace : void 0);
2352
+ } finally {
2353
+ disposeTrace();
2354
+ }
2355
+ }));
2356
+ this._mcp.registerTool("awaitLinkRpcTask", {
2357
+ title: "Wait for a parked linkrpc task",
2358
+ description: "Wait up to `timeoutMs` for a background task started by `runLinkRpcScript` to settle. Returns `{ status: \"completed\" | \"error\" | \"cancelled\" | \"running\", … }`. A `running` result means it is still going (with a progress snapshot) — call again to keep waiting. Does NOT cancel the task.",
2359
+ inputSchema: {
2360
+ taskId: z.string().describe("Task id returned by `runLinkRpcScript`."),
2361
+ timeoutMs: z.number().int().positive().optional().describe("How long to wait before returning a `running` snapshot. Defaults to 30000."),
2362
+ presentation: PRESENTATION_SCHEMA
2363
+ },
2364
+ annotations: { readOnlyHint: true }
2365
+ }, async (args) => this._observeToolCall("awaitLinkRpcTask", args, async () => {
2366
+ const res = await this._tasks.awaitTask(args.taskId, args.timeoutMs ?? 3e4);
2367
+ if (res.status === "unknown") return _errorResult(`No task with id ${args.taskId}`);
2368
+ return _presentTaskResult(res, args.presentation);
2369
+ }));
2370
+ this._mcp.registerTool("cancelLinkRpcTask", {
2371
+ title: "Cancel a parked linkrpc task",
2372
+ description: "Soft-abort a background task started by `runLinkRpcScript` and return its terminal outcome. The guest gets a short grace window to run cleanup.",
2373
+ inputSchema: {
2374
+ taskId: z.string().describe("Task id returned by `runLinkRpcScript`."),
2375
+ presentation: PRESENTATION_SCHEMA
2376
+ },
2377
+ annotations: { readOnlyHint: true }
2378
+ }, async (args) => this._observeToolCall("cancelLinkRpcTask", args, async () => {
2379
+ const res = await this._tasks.cancel(args.taskId);
2380
+ if (res.status === "unknown") return _errorResult(`No task with id ${args.taskId}`);
2381
+ return _presentTaskResult(res, args.presentation);
2382
+ }));
2383
+ }
2384
+ async _observeToolCall(name, args, invoke) {
2385
+ const result = await invoke();
2386
+ try {
2387
+ this._onToolCall?.({
2388
+ name,
2389
+ arguments: args,
2390
+ result
2391
+ });
2392
+ } catch {}
2393
+ return result;
2394
+ }
2395
+ _observeExploreCall(call) {
2396
+ try {
2397
+ this._onExploreCall?.(call);
2398
+ } catch {}
2399
+ }
2400
+ };
2401
+ function _presentTaskResult(value, mode) {
2402
+ try {
2403
+ return presentToolResult(value, mode);
2404
+ } catch (e) {
2405
+ return _errorResult(`Failed to present task result: ${_describeError(e)}`);
2406
+ }
2407
+ }
2408
+ function _errorResult(message, trace) {
2409
+ return {
2410
+ content: [{
2411
+ type: "text",
2412
+ text: trace && trace.length > 0 ? `${message}\n\nTrace:\n${trace.join("\n")}` : message
2413
+ }],
2414
+ isError: true
2415
+ };
2416
+ }
2417
+ /**
2418
+ * Render an unknown thrown value as a debuggable string: the error's stack
2419
+ * (which already begins with `Name: message`) plus the full `cause` chain. The
2420
+ * MCP host only forwards the result text to the model — never a JS stack — so
2421
+ * without this every failure collapses to a single bare message line.
2422
+ */
2423
+ function _describeError(e) {
2424
+ if (!(e instanceof Error)) return String(e);
2425
+ let out = typeof e.stack === "string" && e.stack.length > 0 ? e.stack : e.message;
2426
+ let cause = e.cause;
2427
+ while (cause != null) if (cause instanceof Error) {
2428
+ out += `\n\nCaused by: ${typeof cause.stack === "string" && cause.stack.length > 0 ? cause.stack : cause.message}`;
2429
+ cause = cause.cause;
2430
+ } else {
2431
+ out += `\n\nCaused by: ${String(cause)}`;
2432
+ break;
2433
+ }
2434
+ return out;
2435
+ }
2436
+ /**
2437
+ * Map a hub `permissionRequired` (-32401) error to an actionable message that
2438
+ * points the model at the explicit access flow. Other errors pass through
2439
+ * unchanged (re-wrapped as an `Error` when they are not already one).
2440
+ */
2441
+ function _enrichPermissionError(e, method) {
2442
+ if (_isPermissionError(e)) {
2443
+ const { serviceId, interfaceId, member } = _parseMethod(method);
2444
+ const original = e instanceof Error ? e.message : String(e);
2445
+ return /* @__PURE__ */ new Error(`Permission required for ${method} — no capability is held for this call.\nInspect current access with con.grants(), then request it with con.requestAccess({ permissions: [{ target: { serviceId: { exact: ${JSON.stringify(serviceId)} }, interfaceId: { exact: ${JSON.stringify(interfaceId)} }, members: [{ exact: ${JSON.stringify(member)} }] }, canInvoke: true }], duration: "longLived" }).\nOriginal error: ${original}`);
2446
+ }
2447
+ return e instanceof Error ? e : new Error(String(e));
2448
+ }
2449
+ /** True when `e` is a hub `permissionRequired` (-32401) error. */
2450
+ function _isPermissionError(e) {
2451
+ return e instanceof RpcError ? e.code === ErrorCode.permissionRequired : typeof e === "object" && e !== null && e.code === ErrorCode.permissionRequired;
2452
+ }
2453
+ /**
2454
+ * Split a JSON-RPC method name into its `(serviceId, interfaceId, member)`
2455
+ * parts. Form-3 (`serviceId::interfaceId::member`) keeps the serviceId; form-2
2456
+ * (`interfaceId::member`, a root/hub call) reports an empty serviceId.
2457
+ */
2458
+ function _parseMethod(method) {
2459
+ const parts = method.split("::");
2460
+ if (parts.length >= 3) return {
2461
+ serviceId: parts[0],
2462
+ interfaceId: parts[1],
2463
+ member: parts.slice(2).join("::")
2464
+ };
2465
+ return {
2466
+ serviceId: "",
2467
+ interfaceId: parts[0] ?? "",
2468
+ member: parts[1] ?? ""
2469
+ };
2470
+ }
2471
+ /**
2472
+ * Negotiate durable capabilities for the reflection methods the bus walk uses
2473
+ * — `hubrpc.directory::list` (enumeration) and `hubrpc.schemas::get` (schema
2474
+ * fetch) — across *all* service ids, through the hub's `hubAccess` consent
2475
+ * door. One grant unlocks enumeration of every gated directory the walk
2476
+ * reaches, so `explore({ requestPermission: true })` only needs a single
2477
+ * consent round-trip. Returns `true` when the grant was issued.
2478
+ */
2479
+ async function _requestReflectionAccess(session) {
2480
+ return (await session.requestAccess({
2481
+ consumer: {
2482
+ name: "linkrpc-mcp",
2483
+ purpose: "explore the hub (reflection)"
2484
+ },
2485
+ permissions: [{
2486
+ target: {
2487
+ serviceId: { prefix: "" },
2488
+ interfaceId: { exact: "hubrpc.directory" },
2489
+ members: [{ exact: "list" }]
2490
+ },
2491
+ canInvoke: true
2492
+ }, {
2493
+ target: {
2494
+ serviceId: { prefix: "" },
2495
+ interfaceId: { exact: "hubrpc.schemas" },
2496
+ members: [{ exact: "get" }]
2497
+ },
2498
+ canInvoke: true
2499
+ }],
2500
+ duration: "longLived"
2501
+ })).status === "granted";
2502
+ }
2503
+ /**
2504
+ * Negotiate a durable capability for exactly `method` through the hub's
2505
+ * `hubAccess` consent door (the `requestPermission: true` path of `con.call`).
2506
+ * Returns `true` when the grant was issued — its durable cap joins the
2507
+ * connection's bag, so the caller can retry the call.
2508
+ */
2509
+ async function _autoRequestAccess(session, method) {
2510
+ const { serviceId, interfaceId, member } = _parseMethod(method);
2511
+ return (await session.requestAccess({
2512
+ consumer: {
2513
+ name: "linkrpc-mcp",
2514
+ purpose: `invoke ${method}`
2515
+ },
2516
+ permissions: [{
2517
+ target: {
2518
+ serviceId: { exact: serviceId },
2519
+ interfaceId: { exact: interfaceId },
2520
+ members: [{ exact: member }]
2521
+ },
2522
+ canInvoke: true
2523
+ }],
2524
+ duration: "longLived"
2525
+ })).status === "granted";
2526
+ }
2527
+ //#endregion
2528
+ export { CONNECTION_DTS as a, startSandbox as i, explore as n, ProviderPool as o, runSandboxed as r, ConnectionPool as s, LinkRpcMcpServer as t };
2529
+
2530
+ //# sourceMappingURL=server-C4ZM6bfK.js.map