@animalabs/agent-framework 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/src/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +50 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +125 -23
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +806 -118
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4180 -1121
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +36 -21
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +223 -149
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts +4 -0
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +12 -10
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +84 -9
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -58
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +11 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +7 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +2 -0
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +2 -0
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +4 -2
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +7 -4
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +368 -16
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1224 -136
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +115 -7
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +153 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +140 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +138 -31
- package/src/api/mcp-server-core.ts +10 -3
- package/src/author-presentation.ts +81 -0
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4344 -1052
- package/src/gate/event-gate.ts +251 -150
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +14 -10
- package/src/index.ts +16 -2
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/channel-registry.ts +170 -60
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +21 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/toolsets-module.ts +7 -0
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/notice-tags.ts +2 -0
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/silent-heartbeat.ts +7 -4
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +1374 -141
- package/src/tune-out/tools.ts +166 -13
- package/src/types/agent.ts +30 -1
- package/src/types/framework.ts +157 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +154 -3
|
@@ -315,6 +315,13 @@ export class ToolsetsModule implements Module {
|
|
|
315
315
|
const args = isObj(call.input) ? call.input : {};
|
|
316
316
|
try {
|
|
317
317
|
checkSelectors(args);
|
|
318
|
+
// An agent shown another's tool list can't switch it: what it opened
|
|
319
|
+
// or closed would change its owner's list, or nothing it is shown.
|
|
320
|
+
const caller = call.callerAgentName;
|
|
321
|
+
const owner = caller && this.host ? this.host.ownerOf(caller) : caller;
|
|
322
|
+
if ((call.name === 'open' || call.name === 'close') && caller && owner !== caller) {
|
|
323
|
+
fail(`${caller} is shown ${owner}'s tools and can't ${call.name} a toolset; it reaches the toolsets ${owner} has open. Nothing changed.`);
|
|
324
|
+
}
|
|
318
325
|
const hosted = await this.hostCall(call.name, args, call.callerAgentName);
|
|
319
326
|
if (hosted) return hosted;
|
|
320
327
|
const take = this.takerFor(call);
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -22,11 +22,11 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
|
|
22
22
|
import { EventEmitter } from 'node:events';
|
|
23
23
|
import type { Readable } from 'node:stream';
|
|
24
24
|
import { StringDecoder } from 'node:string_decoder';
|
|
25
|
-
import { escapeLineSeparators, readLines } from '../line-reader.js';
|
|
25
|
+
import { MAX_LINE_CHARS, escapeLineSeparators, readLines } from '../line-reader.js';
|
|
26
26
|
import WebSocket from 'ws';
|
|
27
27
|
|
|
28
28
|
import type { McplServerConfig } from './types.js';
|
|
29
|
-
import { WITHHELD_TEXT, describeCredentialError, headerCarries, maskedError, noteDialSent, redactServerText, redactUrl, resolveServerBinding, serverTextRedactor, tokenTransportProblem, urlAsDialed, type Redaction } from './protocol-family.js';
|
|
29
|
+
import { WITHHELD_TEXT, describeCredentialError, headerCarries, maskedError, noteDialSent, redactServerText, redactUrl, resolveServerBinding, serverConfigProblems, serverTextRedactor, tokenTransportProblem, urlAsDialed, type Redaction } from './protocol-family.js';
|
|
30
30
|
import { assertBooleanSetting } from '../config-checks.js';
|
|
31
31
|
import { logDetail } from './log-value.js';
|
|
32
32
|
|
|
@@ -39,6 +39,11 @@ export interface TransportCloseInfo {
|
|
|
39
39
|
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
40
40
|
* reason, rather than the transport's words. */
|
|
41
41
|
reasonFromPeer?: boolean;
|
|
42
|
+
/** True when a stdio transport ended its child itself, after an error on
|
|
43
|
+
* the child's stdin or stdout or a line on its stdout longer than the
|
|
44
|
+
* bound: the exit's signal or code is then this transport's doing, not a
|
|
45
|
+
* stop asked of the server. */
|
|
46
|
+
pipeFailed?: boolean;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -49,20 +54,16 @@ export interface TransportCloseInfo {
|
|
|
49
54
|
* - `'stderr'` `(line: string)` diagnostic line (stdio child stderr; never WS)
|
|
50
55
|
* - `'error'` `(err: Error)` transport-level error
|
|
51
56
|
* - `'close'` `(info: TransportCloseInfo)` transport closed (once)
|
|
52
|
-
* - `'unreaped'` `(failure: Error)` a stdio child
|
|
53
|
-
* outlived SIGKILL too: the transport stays open, owning the
|
|
54
|
-
* close() retries the reap (never WS)
|
|
55
|
-
*
|
|
56
|
-
* Inbound `'line'`/`'stderr'` events are buffered until the first matching
|
|
57
|
-
* listener attaches (and re-buffered whenever there are none), so no message is
|
|
58
|
-
* lost across the handshake→routing handoff regardless of attach timing.
|
|
57
|
+
* - `'unreaped'` `(failure: Error)` a stdio child this transport was
|
|
58
|
+
* ending outlived SIGKILL too: the transport stays open, owning the
|
|
59
|
+
* child, and close() retries the reap (never WS)
|
|
59
60
|
*/
|
|
60
61
|
export abstract class McplTransport extends EventEmitter {
|
|
61
62
|
abstract readonly kind: 'stdio' | 'websocket';
|
|
62
63
|
|
|
63
64
|
private _closed = false;
|
|
64
65
|
private lineBuffer: string[] = [];
|
|
65
|
-
private
|
|
66
|
+
private readonly heldStderr = new HeldStderr();
|
|
66
67
|
|
|
67
68
|
/** The server's id from its config, so a line the transport logs names it.
|
|
68
69
|
* The stdio and WebSocket transports always give it; a test's stand-in
|
|
@@ -75,9 +76,8 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
75
76
|
const pending = this.lineBuffer;
|
|
76
77
|
this.lineBuffer = [];
|
|
77
78
|
queueMicrotask(() => { for (const l of pending) this.emit('line', l); });
|
|
78
|
-
} else if (event === 'stderr' && this.
|
|
79
|
-
const pending = this.
|
|
80
|
-
this.stderrBuffer = [];
|
|
79
|
+
} else if (event === 'stderr' && !this.heldStderr.empty) {
|
|
80
|
+
const pending = this.heldStderr.take();
|
|
81
81
|
queueMicrotask(() => { for (const l of pending) this.emit('stderr', l); });
|
|
82
82
|
}
|
|
83
83
|
});
|
|
@@ -112,14 +112,7 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
112
112
|
return '';
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
* The error a stdio child's stdin or stdout failed with, once one has. The
|
|
117
|
-
* transport is ending the child then, and stays open until it exits, but
|
|
118
|
-
* writes nothing more to it: writeLine drops each line, and both engines
|
|
119
|
-
* refuse a request as not sent. Undefined while writes go out, and always
|
|
120
|
-
* for a transport that has no pipes.
|
|
121
|
-
*/
|
|
122
|
-
get pipeFailure(): Error | undefined {
|
|
115
|
+
get stdioStopCause(): Error | undefined {
|
|
123
116
|
return undefined;
|
|
124
117
|
}
|
|
125
118
|
|
|
@@ -141,7 +134,7 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
141
134
|
/** Deliver a diagnostic line, buffering it if nobody is listening yet. */
|
|
142
135
|
protected pushStderr(line: string): void {
|
|
143
136
|
if (this.listenerCount('stderr') > 0) this.emit('stderr', line);
|
|
144
|
-
else this.
|
|
137
|
+
else this.heldStderr.hold(line);
|
|
145
138
|
}
|
|
146
139
|
|
|
147
140
|
/**
|
|
@@ -277,8 +270,8 @@ export const STDERR_END_WAIT_MS = 1_000;
|
|
|
277
270
|
|
|
278
271
|
export class StdioTransport extends McplTransport {
|
|
279
272
|
readonly kind = 'stdio' as const;
|
|
280
|
-
|
|
281
|
-
private
|
|
273
|
+
private stopCause: Error | undefined;
|
|
274
|
+
private endedChild = false;
|
|
282
275
|
/** The stream's last STDERR_WINDOW_CHARS, raw. */
|
|
283
276
|
private stderrWindow = '';
|
|
284
277
|
/** True once the child's stderr has ended or closed. */
|
|
@@ -303,8 +296,8 @@ export class StdioTransport extends McplTransport {
|
|
|
303
296
|
return (cut ? '…' : '') + kept;
|
|
304
297
|
}
|
|
305
298
|
|
|
306
|
-
override get
|
|
307
|
-
return this.
|
|
299
|
+
override get stdioStopCause(): Error | undefined {
|
|
300
|
+
return this.stopCause;
|
|
308
301
|
}
|
|
309
302
|
|
|
310
303
|
override stderrEnded(ms: number): Promise<void> {
|
|
@@ -337,25 +330,48 @@ export class StdioTransport extends McplTransport {
|
|
|
337
330
|
) {
|
|
338
331
|
super(serverId);
|
|
339
332
|
|
|
340
|
-
|
|
333
|
+
const lineBound = testLineBound ?? MAX_LINE_CHARS;
|
|
334
|
+
this.stopLines = readLines(stdout, {
|
|
335
|
+
line: (line) => this.pushLine(line),
|
|
336
|
+
tooLong: (maxChars) => this.stopStdio(new Error(`the server wrote a line on stdout longer than ${maxChars.toLocaleString('en-US')} UTF-16 code units`), false),
|
|
337
|
+
}, lineBound);
|
|
341
338
|
|
|
342
|
-
// Stitch stderr chunks into whole lines (a chunk can split a line, and a
|
|
343
|
-
// character's bytes), and keep the stream's last characters for
|
|
344
|
-
// stderrTail.
|
|
345
339
|
let carry = '';
|
|
340
|
+
let dropping = false;
|
|
341
|
+
const dropped = `[a stderr line longer than ${lineBound.toLocaleString('en-US')} UTF-16 code units was dropped]`;
|
|
346
342
|
const decoder = new StringDecoder('utf8');
|
|
347
343
|
const received = (text: string) => {
|
|
348
344
|
const window = this.stderrWindow + text;
|
|
349
345
|
this.stderrWindow = window.slice(-STDERR_WINDOW_CHARS);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
346
|
+
let rest = text;
|
|
347
|
+
if (dropping) {
|
|
348
|
+
const end = rest.indexOf('\n');
|
|
349
|
+
if (end === -1) return;
|
|
350
|
+
dropping = false;
|
|
351
|
+
rest = rest.slice(end + 1);
|
|
352
|
+
}
|
|
353
|
+
if (rest.includes('\n')) {
|
|
354
|
+
const lines = (carry + rest).split('\n');
|
|
355
|
+
carry = lines.pop() ?? '';
|
|
356
|
+
for (const l of lines) {
|
|
357
|
+
if (l.length > lineBound) this.pushStderr(dropped);
|
|
358
|
+
else if (l.length > 0) this.pushStderr(l);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
carry += rest;
|
|
362
|
+
}
|
|
363
|
+
if (carry.length > lineBound) {
|
|
364
|
+
carry = '';
|
|
365
|
+
dropping = true;
|
|
366
|
+
this.pushStderr(dropped);
|
|
367
|
+
}
|
|
353
368
|
};
|
|
354
369
|
this.child.stderr?.on('data', (chunk: Buffer) => received(decoder.write(chunk)));
|
|
355
370
|
// A last line with no newline is complete once the stream ends.
|
|
356
371
|
this.child.stderr?.on('end', () => {
|
|
357
372
|
received(decoder.end());
|
|
358
373
|
if (carry.length > 0) this.pushStderr(carry);
|
|
374
|
+
carry = '';
|
|
359
375
|
this.stderrDone = true;
|
|
360
376
|
});
|
|
361
377
|
// A stream destroyed by an error closes without ending: nothing more comes.
|
|
@@ -364,13 +380,17 @@ export class StdioTransport extends McplTransport {
|
|
|
364
380
|
this.child.on('error', (err) => this.reportError(err));
|
|
365
381
|
// The child's stdin and stdout carry the protocol, so after an error on
|
|
366
382
|
// either, a write's EPIPE or a read error, this transport can't reach the
|
|
367
|
-
// child even if it's still running.
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
383
|
+
// child even if it's still running. Nothing more is written (stdioStopCause),
|
|
384
|
+
// and the handler first gives the child its exit grace to exit on its
|
|
385
|
+
// own: a child already dead when the write failed, as one a stop's signal
|
|
386
|
+
// reached first, exits within it, and its close carries its own code and
|
|
387
|
+
// signal. One still running then is ended as close() ends it, SIGTERM and
|
|
388
|
+
// then SIGKILL, in the one attempt close() shares, and only that close is
|
|
389
|
+
// pipeFailed, since its signal is this transport's. Either way the exit's
|
|
390
|
+
// close names the error, so the connection handles it as any other exit.
|
|
391
|
+
// The close is the report: no 'error' is emitted and nothing is thrown.
|
|
392
|
+
// The first error starts the wait, and a later one, from either pipe,
|
|
393
|
+
// adds nothing to it. A child that outlives SIGKILL too stays this
|
|
374
394
|
// transport's: 'unreaped' says so, unless the host is closing it, whose
|
|
375
395
|
// close() reports the failure itself. A child that stops reading without
|
|
376
396
|
// closing its stdin raises no error: the writes are buffered, and only
|
|
@@ -378,15 +398,8 @@ export class StdioTransport extends McplTransport {
|
|
|
378
398
|
// Bun a child that closes its stdin isn't reported either: Bun emits no
|
|
379
399
|
// 'error' for the writes to it (checked on 1.3.10, 1.4.3 and 1.4.4's
|
|
380
400
|
// canary), so request timeouts are its only bound too.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.pipeError = err;
|
|
384
|
-
void this.terminate().catch((failure: unknown) => {
|
|
385
|
-
if (!this.closed) this.emit('unreaped', failure instanceof Error ? failure : new Error(String(failure)));
|
|
386
|
-
});
|
|
387
|
-
};
|
|
388
|
-
this.child.stdin?.on('error', endOnPipeError);
|
|
389
|
-
this.child.stdout?.on('error', endOnPipeError);
|
|
401
|
+
this.child.stdin?.on('error', (err) => this.stopStdio(err, true));
|
|
402
|
+
this.child.stdout?.on('error', (err) => this.stopStdio(err, true));
|
|
390
403
|
// stderr carries diagnostics only: a read error on it is reported, to the
|
|
391
404
|
// listeners or else in the log, and never thrown. It ends nothing once
|
|
392
405
|
// the connection is wired; during a handshake the listener is the
|
|
@@ -395,10 +408,24 @@ export class StdioTransport extends McplTransport {
|
|
|
395
408
|
this.child.on('exit', (code, signal) => this.markClosed({
|
|
396
409
|
code,
|
|
397
410
|
signal,
|
|
398
|
-
reason: this.
|
|
411
|
+
reason: this.stopCause ? `child process exited, and this host had stopped using the server's stdio (${this.stopCause.message})` : 'child process exited',
|
|
412
|
+
...(this.endedChild ? { pipeFailed: true } : {}),
|
|
399
413
|
}));
|
|
400
414
|
}
|
|
401
415
|
|
|
416
|
+
private stopStdio(cause: Error, exitGraceFirst: boolean): void {
|
|
417
|
+
if (this.stopCause) return;
|
|
418
|
+
this.stopCause = cause;
|
|
419
|
+
const exitedFirst = exitGraceFirst ? this.exitWithin(this.exitGrace()) : Promise.resolve(this.exited());
|
|
420
|
+
void exitedFirst.then((gone) => {
|
|
421
|
+
if (gone || this.terminating) return;
|
|
422
|
+
this.endedChild = true;
|
|
423
|
+
return this.terminate();
|
|
424
|
+
}).catch((failure: unknown) => {
|
|
425
|
+
if (!this.closed) this.emit('unreaped', failure instanceof Error ? failure : new Error(String(failure)));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
402
429
|
/**
|
|
403
430
|
* Spawn `config.command` and resolve with a ready stdio transport once the
|
|
404
431
|
* child has spawned. Rejects if the command is missing (a config bug) or the
|
|
@@ -455,9 +482,7 @@ export class StdioTransport extends McplTransport {
|
|
|
455
482
|
}
|
|
456
483
|
|
|
457
484
|
writeLine(json: string): void {
|
|
458
|
-
|
|
459
|
-
// transport is ending it.
|
|
460
|
-
if (this.pipeError) return;
|
|
485
|
+
if (this.stopCause) return;
|
|
461
486
|
// stdin may be gone if the child died between the closed-check and here.
|
|
462
487
|
this.child.stdin?.write(escapeLineSeparators(json) + '\n');
|
|
463
488
|
}
|
|
@@ -493,6 +518,22 @@ export class StdioTransport extends McplTransport {
|
|
|
493
518
|
await this.terminate();
|
|
494
519
|
}
|
|
495
520
|
|
|
521
|
+
/** How long the child gets to exit after a pipe error, after SIGTERM and
|
|
522
|
+
* after SIGKILL: {@link STDIO_EXIT_GRACE_MS}, or a test's grace for it. */
|
|
523
|
+
private exitGrace(): number {
|
|
524
|
+
return testExitGraces.get(this.child) ?? STDIO_EXIT_GRACE_MS;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Whether the child exits within `ms`: true at once if it has. */
|
|
528
|
+
private exitWithin(ms: number): Promise<boolean> {
|
|
529
|
+
if (this.exited()) return Promise.resolve(true);
|
|
530
|
+
return new Promise((resolve) => {
|
|
531
|
+
const onExit = () => { clearTimeout(timer); resolve(true); };
|
|
532
|
+
const timer = setTimeout(() => { this.child.off('exit', onExit); resolve(false); }, ms);
|
|
533
|
+
this.child.once('exit', onExit);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
496
537
|
/** Whether the child has exited, or never started (a spawn error). */
|
|
497
538
|
private exited(): boolean {
|
|
498
539
|
return this.child.pid === undefined || this.child.exitCode !== null || this.child.signalCode !== null;
|
|
@@ -518,26 +559,82 @@ export class StdioTransport extends McplTransport {
|
|
|
518
559
|
private async escalate(): Promise<void> {
|
|
519
560
|
const child = this.child;
|
|
520
561
|
if (this.exited()) return;
|
|
562
|
+
const grace = this.exitGrace();
|
|
521
563
|
const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()));
|
|
522
564
|
const exitWithin = (ms: number) => new Promise<boolean>((resolve) => {
|
|
523
565
|
const timer = setTimeout(() => resolve(false), ms);
|
|
524
566
|
void exited.then(() => { clearTimeout(timer); resolve(true); });
|
|
525
567
|
});
|
|
526
568
|
child.kill('SIGTERM');
|
|
527
|
-
if (await exitWithin(
|
|
569
|
+
if (await exitWithin(grace)) return;
|
|
528
570
|
const named = `stdio child ${child.pid} of MCP server "${this.serverId}"`;
|
|
529
|
-
console.error(`[mcpl] ${named} ignored SIGTERM for ${
|
|
571
|
+
console.error(`[mcpl] ${named} ignored SIGTERM for ${grace}ms — sending SIGKILL`);
|
|
530
572
|
child.kill('SIGKILL');
|
|
531
|
-
if (await exitWithin(
|
|
573
|
+
if (await exitWithin(grace)) return;
|
|
532
574
|
throw new Error(
|
|
533
|
-
`${named} did not exit within ${
|
|
575
|
+
`${named} did not exit within ${grace}ms of SIGKILL; it could not be reaped`,
|
|
534
576
|
);
|
|
535
577
|
}
|
|
536
578
|
}
|
|
537
579
|
|
|
538
|
-
/** How long a stdio child gets to exit after
|
|
580
|
+
/** How long a stdio child gets to exit on its own after a pipe error, after
|
|
581
|
+
* SIGTERM, and again after SIGKILL. */
|
|
539
582
|
const STDIO_EXIT_GRACE_MS = 2_000;
|
|
540
583
|
|
|
584
|
+
const testExitGraces = new WeakMap<ChildProcess, number>();
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* For tests only: `child` gets `ms` in place of {@link STDIO_EXIT_GRACE_MS}.
|
|
588
|
+
* Set it only on a child that can exit by no means, such as one whose
|
|
589
|
+
* `kill()` is a no-op and that nothing else ends while it's closed: a short
|
|
590
|
+
* grace there races no exit, and only brings "could not be reaped" sooner.
|
|
591
|
+
*/
|
|
592
|
+
export function setStdioExitGraceForTests(child: ChildProcess, ms: number): void {
|
|
593
|
+
testExitGraces.set(child, ms);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let testLineBound: number | undefined;
|
|
597
|
+
|
|
598
|
+
const MAX_HELD_STDERR_LINES = 200;
|
|
599
|
+
|
|
600
|
+
export class HeldStderr {
|
|
601
|
+
private lines: string[] = [];
|
|
602
|
+
private chars = 0;
|
|
603
|
+
private dropped = 0;
|
|
604
|
+
|
|
605
|
+
get empty(): boolean {
|
|
606
|
+
return this.lines.length === 0 && this.dropped === 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
hold(line: string): void {
|
|
610
|
+
// V8 can make a line split from a longer string a slice that keeps the
|
|
611
|
+
// whole string alive, so a held line is copied to own only its characters.
|
|
612
|
+
this.lines.push(Buffer.from(line, 'utf16le').toString('utf16le'));
|
|
613
|
+
this.chars += line.length;
|
|
614
|
+
const ceiling = testLineBound ?? MAX_LINE_CHARS;
|
|
615
|
+
while (this.lines.length > MAX_HELD_STDERR_LINES || this.chars > ceiling) {
|
|
616
|
+
this.chars -= this.lines.shift()!.length;
|
|
617
|
+
this.dropped++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
take(): string[] {
|
|
622
|
+
const held = this.lines;
|
|
623
|
+
const taken = this.dropped > 0
|
|
624
|
+
? [`[${this.dropped.toLocaleString('en-US')} earlier stderr line${this.dropped === 1 ? ' was' : 's were'} dropped: unread stderr is held to ${MAX_HELD_STDERR_LINES} lines and ${(testLineBound ?? MAX_LINE_CHARS).toLocaleString('en-US')} UTF-16 code units]`, ...held]
|
|
625
|
+
: held;
|
|
626
|
+
this.lines = [];
|
|
627
|
+
this.chars = 0;
|
|
628
|
+
this.dropped = 0;
|
|
629
|
+
return taken;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** @internal */
|
|
634
|
+
export function setStdioLineBoundForTests(maxChars: number | undefined): void {
|
|
635
|
+
testLineBound = maxChars;
|
|
636
|
+
}
|
|
637
|
+
|
|
541
638
|
// ---------------------------------------------------------------------------
|
|
542
639
|
// WebSocket
|
|
543
640
|
// ---------------------------------------------------------------------------
|
|
@@ -594,6 +691,8 @@ export class WebSocketTransport extends McplTransport {
|
|
|
594
691
|
* at the open timeout.
|
|
595
692
|
*/
|
|
596
693
|
static async open(config: McplServerConfig, signal?: AbortSignal): Promise<WebSocketTransport> {
|
|
694
|
+
const credentialRequired = checkedCredentialRequired(config);
|
|
695
|
+
if (credentialRequired === true) return WebSocketTransport.openStrict(config, signal);
|
|
597
696
|
// Resolve the per-dial credential first: the provider runs on every
|
|
598
697
|
// connect AND reconnect, so rotation happens at the transport layer and
|
|
599
698
|
// nothing above it ever holds a credential.
|
|
@@ -691,6 +790,97 @@ export class WebSocketTransport extends McplTransport {
|
|
|
691
790
|
});
|
|
692
791
|
}
|
|
693
792
|
|
|
793
|
+
/** Separate opt-in path: the legacy dial above keeps its objects, reads,
|
|
794
|
+
* receivers and error behavior. Capture policy before calling host code;
|
|
795
|
+
* only noteDialSent uses the original config after awaiting (as a WeakMap
|
|
796
|
+
* key, without reading it). No provider result is stored on either config. */
|
|
797
|
+
private static async openStrict(config: McplServerConfig, signal?: AbortSignal): Promise<WebSocketTransport> {
|
|
798
|
+
const captured: McplServerConfig = {
|
|
799
|
+
id: config.id, url: config.url, tokenTransport: config.tokenTransport,
|
|
800
|
+
credentialRequired: true, token: config.token, accessProvider: config.accessProvider,
|
|
801
|
+
command: config.command, transport: config.transport, protocol: config.protocol,
|
|
802
|
+
};
|
|
803
|
+
const id = captured.id;
|
|
804
|
+
const fail = (reason: 'provider-failed' | 'credential-rejected' | 'engine-unsupported' | 'connection-failed') =>
|
|
805
|
+
new Error(`MCPL server "${id}": ${reason}`);
|
|
806
|
+
let problems: string[];
|
|
807
|
+
try { problems = serverConfigProblems(captured); }
|
|
808
|
+
catch { throw fail('connection-failed'); }
|
|
809
|
+
if (problems.length) throw fail('connection-failed');
|
|
810
|
+
const provider = captured.accessProvider;
|
|
811
|
+
if (typeof provider !== 'function') throw fail('provider-failed');
|
|
812
|
+
const followRedirects = false;
|
|
813
|
+
let url: string;
|
|
814
|
+
try { url = buildWebSocketUrl(captured, null); }
|
|
815
|
+
catch { throw fail('connection-failed'); }
|
|
816
|
+
if (signal?.aborted) throw fail('connection-failed');
|
|
817
|
+
let token: unknown;
|
|
818
|
+
try {
|
|
819
|
+
// Preserve the provider's receiver, without re-reading any config input.
|
|
820
|
+
const provided = Promise.resolve(provider.call(config));
|
|
821
|
+
provided.catch(() => { /* ignored after abandonment */ });
|
|
822
|
+
token = signal ? await raceAbort(provided, signal) : await provided;
|
|
823
|
+
} catch {
|
|
824
|
+
if (signal?.aborted) throw fail('connection-failed');
|
|
825
|
+
throw fail('provider-failed');
|
|
826
|
+
}
|
|
827
|
+
if (signal?.aborted) throw fail('connection-failed');
|
|
828
|
+
if (typeof token !== 'string' || !headerCarries(token)) throw fail('provider-failed');
|
|
829
|
+
noteDialSent(config, { url, token });
|
|
830
|
+
let ws: WebSocket;
|
|
831
|
+
try {
|
|
832
|
+
ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` }, followRedirects });
|
|
833
|
+
} catch { throw fail('connection-failed'); }
|
|
834
|
+
const swallowLateErrors = () => { /* settled; retry loop owns recovery */ };
|
|
835
|
+
return new Promise<WebSocketTransport>((resolve, reject) => {
|
|
836
|
+
let settled = false;
|
|
837
|
+
const abandon = (reason: 'connection-failed' | 'credential-rejected' | 'engine-unsupported' = 'connection-failed',
|
|
838
|
+
request?: { destroy?: () => void; abort?: () => void }, response?: { destroy: () => void }): void => {
|
|
839
|
+
if (settled) return;
|
|
840
|
+
settled = true;
|
|
841
|
+
clearTimeout(timer);
|
|
842
|
+
signal?.removeEventListener('abort', onAbort);
|
|
843
|
+
ws.removeAllListeners();
|
|
844
|
+
ws.on('error', swallowLateErrors);
|
|
845
|
+
// The status listener suppresses ws's default abort. Discard the body
|
|
846
|
+
// without inspecting it, and use the request API this engine offers.
|
|
847
|
+
try { response?.destroy(); } catch { /* already gone */ }
|
|
848
|
+
try {
|
|
849
|
+
if (typeof request?.destroy === 'function') request.destroy();
|
|
850
|
+
else request?.abort?.(); // Some clients expose abort(), not destroy().
|
|
851
|
+
} catch { /* already gone */ }
|
|
852
|
+
try { ws.terminate(); } catch { /* already gone */ }
|
|
853
|
+
reject(fail(reason));
|
|
854
|
+
};
|
|
855
|
+
const timer = setTimeout(() => abandon(), WS_OPEN_TIMEOUT_MS);
|
|
856
|
+
const onAbort = (): void => abandon();
|
|
857
|
+
if (signal?.aborted) { onAbort(); return; }
|
|
858
|
+
else signal?.addEventListener('abort', onAbort, { once: true });
|
|
859
|
+
// Installed only on this explicit strict path. No peer text is a status.
|
|
860
|
+
ws.once('unexpected-response', (request, response) => {
|
|
861
|
+
const status = response?.statusCode;
|
|
862
|
+
// A refused upgrade without structured status cannot classify auth.
|
|
863
|
+
// Generic socket errors still mean connection-failed, never parsed text.
|
|
864
|
+
abandon(typeof status !== 'number' || !Number.isFinite(status) ? 'engine-unsupported'
|
|
865
|
+
: status === 401 || status === 403 ? 'credential-rejected' : 'connection-failed', request, response);
|
|
866
|
+
});
|
|
867
|
+
ws.once('open', () => {
|
|
868
|
+
if (settled) return;
|
|
869
|
+
settled = true;
|
|
870
|
+
clearTimeout(timer);
|
|
871
|
+
signal?.removeEventListener('abort', onAbort);
|
|
872
|
+
ws.removeAllListeners('unexpected-response');
|
|
873
|
+
const transport = new WebSocketTransport(ws, captured, url);
|
|
874
|
+
// Strict transport-created errors carry only id and a fixed code;
|
|
875
|
+
// don't project a native error or its cause into them.
|
|
876
|
+
ws.removeAllListeners('error');
|
|
877
|
+
ws.on('error', () => transport.reportError(fail('connection-failed')));
|
|
878
|
+
resolve(transport);
|
|
879
|
+
});
|
|
880
|
+
ws.once('error', () => abandon());
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
694
884
|
writeLine(json: string): void {
|
|
695
885
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
696
886
|
this.ws.send(escapeLineSeparators(json));
|
|
@@ -824,15 +1014,25 @@ type BooleanSetting = {
|
|
|
824
1014
|
* boolean setting added to McplServerConfig doesn't compile until it has
|
|
825
1015
|
* an entry here, so the check below covers it too. */
|
|
826
1016
|
const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
|
|
1017
|
+
credentialRequired: 'true requires a provider credential on every WebSocket dial without fallback; false or leaving it out keeps legacy behavior',
|
|
827
1018
|
inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
|
|
828
1019
|
reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
|
|
829
1020
|
allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
|
|
830
1021
|
toolHints: 'true advertises MCPL tool hints (RFC-014) to an MCPL server, so its dynamic tools are announced in the conversation and called through toolsets--act; leaving it out does the same wherever the name toolsets is free; false lists every tool as before and advertises none',
|
|
831
1022
|
};
|
|
832
1023
|
|
|
1024
|
+
/** Direct transport callers bypass config validation, but not this check. */
|
|
1025
|
+
function checkedCredentialRequired(config: McplServerConfig): boolean | undefined {
|
|
1026
|
+
const value = config.credentialRequired;
|
|
1027
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
1028
|
+
assertBooleanSetting(`MCPL server "${config.id}" credentialRequired`, value, SERVER_CONFIG_BOOLEANS.credentialRequired);
|
|
1029
|
+
}
|
|
1030
|
+
return value;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
833
1033
|
/**
|
|
834
|
-
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands
|
|
835
|
-
* or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
1034
|
+
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands`,
|
|
1035
|
+
* `credentialRequired` or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
836
1036
|
* "false" where the type says boolean, and every reader of these settings takes
|
|
837
1037
|
* only `true` as on, so the quotes would hide what the value does. null counts
|
|
838
1038
|
* as not set, so a generated config that writes an unset setting as null
|
|
@@ -864,6 +1064,7 @@ export function assertServerConfigBooleans(config: McplServerConfig): void {
|
|
|
864
1064
|
* performed the MCPL handshake.
|
|
865
1065
|
*/
|
|
866
1066
|
export async function openTransport(config: McplServerConfig, signal?: AbortSignal): Promise<McplTransport> {
|
|
1067
|
+
if (checkedCredentialRequired(config) === true) return WebSocketTransport.open(config, signal);
|
|
867
1068
|
const binding = resolveServerBinding(config);
|
|
868
1069
|
if (binding.family !== 'legacy') {
|
|
869
1070
|
throw new Error(`MCP server "${config.id}" is configured for modern MCP; the legacy transports can't open it`);
|
package/src/mcpl/types.ts
CHANGED
|
@@ -313,10 +313,27 @@ export interface McplServerConfig {
|
|
|
313
313
|
*/
|
|
314
314
|
tokenTransport?: 'query' | 'header';
|
|
315
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Require a provider credential on every WebSocket dial, including reconnect.
|
|
318
|
+
* Only true enables this; false, null and omission preserve optional credentials.
|
|
319
|
+
* Requires tokenTransport 'header', no static token (even empty), and no URL
|
|
320
|
+
* token parameter, userinfo or fragment. HTTP, stdio and modern servers cannot
|
|
321
|
+
* use this option. Cleartext ws:// is allowed only to localhost or a literal
|
|
322
|
+
* loopback address.
|
|
323
|
+
* Missing, throwing, empty or non-sendable providers fail without connecting;
|
|
324
|
+
* printable ASCII credentials with no space at either end have no length floor.
|
|
325
|
+
* Redirects are never followed. Refused upgrades need a finite numeric status:
|
|
326
|
+
* 401/403 yield credential-rejected; missing status yields engine-unsupported.
|
|
327
|
+
* Provider failures yield provider-failed; other dial failures yield
|
|
328
|
+
* connection-failed. Diagnostic redaction is unchanged.
|
|
329
|
+
*/
|
|
330
|
+
credentialRequired?: boolean;
|
|
331
|
+
|
|
316
332
|
/**
|
|
317
333
|
* Lazily resolves the connection credential at EVERY dial — first connect
|
|
318
334
|
* and each background reconnect — overriding `token` when it returns a
|
|
319
|
-
* value (null/throw falls back to `token
|
|
335
|
+
* value (null/throw falls back to `token`, except under `credentialRequired`,
|
|
336
|
+
* where they fail the dial). Host-attached plumbing, never
|
|
320
337
|
* serialized: the point is that credentials stay out of the agent's
|
|
321
338
|
* context entirely (the agent asks for access by name; the host fetches
|
|
322
339
|
* something fresh each redial, which also makes short-lived credentials
|