@jini-ai/mcp 0.3.4 → 0.4.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/CHANGELOG.md +151 -0
- package/README.md +97 -22
- package/dist/agent-install/install.d.ts +20 -4
- package/dist/agent-install/install.js +11 -11
- package/dist/bin/serve.d.ts +25 -13
- package/dist/bin/serve.js +42 -21
- package/dist/client/client.d.ts +28 -8
- package/dist/client/client.js +25 -21
- package/dist/core/config.d.ts +57 -18
- package/dist/core/config.js +76 -48
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/install-info.d.ts +4 -1
- package/dist/core/install-info.js +5 -5
- package/dist/core/tokens.d.ts +39 -13
- package/dist/core/tokens.js +54 -24
- package/dist/federation/adapter.http.d.ts +67 -0
- package/dist/federation/adapter.http.js +414 -0
- package/dist/federation/approvals/call-confirmation.d.ts +23 -0
- package/dist/federation/approvals/call-confirmation.js +124 -0
- package/dist/federation/approvals/index.d.ts +4 -0
- package/dist/federation/approvals/index.js +4 -0
- package/dist/federation/approvals/ports.d.ts +107 -0
- package/dist/federation/approvals/ports.js +2 -0
- package/dist/federation/approvals/revocation.d.ts +61 -0
- package/dist/federation/approvals/revocation.js +52 -0
- package/dist/federation/bootstrap.d.ts +100 -0
- package/dist/federation/bootstrap.js +182 -0
- package/dist/federation/config.d.ts +68 -0
- package/dist/federation/config.js +40 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.js +14 -0
- package/dist/federation/mcp-protocol.d.ts +115 -0
- package/dist/federation/mcp-protocol.js +151 -0
- package/dist/federation/messages.d.ts +54 -0
- package/dist/federation/messages.js +35 -0
- package/dist/federation/ports.d.ts +287 -0
- package/dist/federation/ports.js +5 -0
- package/dist/federation/presets.d.ts +66 -0
- package/dist/federation/presets.js +30 -0
- package/dist/federation/refusal-notice.d.ts +198 -0
- package/dist/federation/refusal-notice.js +193 -0
- package/dist/federation/registrations.d.ts +171 -0
- package/dist/federation/registrations.js +181 -0
- package/dist/federation/reload.d.ts +112 -0
- package/dist/federation/reload.js +110 -0
- package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
- package/dist/federation/stdio/adapter.stdio.js +526 -0
- package/dist/federation/stdio/default-connect.d.ts +29 -0
- package/dist/federation/stdio/default-connect.js +92 -0
- package/dist/federation/stdio/index.d.ts +4 -0
- package/dist/federation/stdio/index.js +4 -0
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
- package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
- package/dist/federation/testing/adapter.memory.d.ts +177 -0
- package/dist/federation/testing/adapter.memory.js +189 -0
- package/dist/federation/testing/index.d.ts +3 -0
- package/dist/federation/testing/index.js +3 -0
- package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
- package/dist/federation/testing/tool-approvals.memory.js +38 -0
- package/dist/federation/tool-approvals.d.ts +97 -0
- package/dist/federation/tool-approvals.js +42 -0
- package/dist/federation/trust.d.ts +471 -0
- package/dist/federation/trust.js +707 -0
- package/dist/index.d.ts +175 -2
- package/dist/index.js +172 -2
- package/dist/server/daemon-client.d.ts +16 -4
- package/dist/server/daemon-client.js +41 -19
- package/dist/server/ports.d.ts +9 -0
- package/dist/server/ports.js +2 -0
- package/dist/server/resource-protocol.d.ts +14 -4
- package/dist/server/resource-protocol.js +6 -6
- package/dist/server/resources/active-resource.js +3 -3
- package/dist/server/sdk-transport.d.ts +11 -0
- package/dist/server/sdk-transport.js +31 -0
- package/dist/server/tool-protocol.d.ts +31 -9
- package/dist/server/tool-protocol.js +20 -18
- package/dist/server/tool-server.d.ts +60 -43
- package/dist/server/tool-server.js +51 -27
- package/dist/server/tools/component-catalog-tools.js +7 -7
- package/dist/server/tools/delegated-tool.d.ts +7 -4
- package/dist/server/tools/delegated-tool.js +14 -15
- package/dist/server/tools/run-tools.js +15 -15
- package/dist/server/tools/tool-catalog-tools.js +7 -7
- package/dist/tools/ask-choice/index.d.ts +123 -0
- package/dist/tools/ask-choice/index.js +98 -0
- package/dist/tools/ask-choice/pending.d.ts +11 -0
- package/dist/tools/ask-choice/pending.js +34 -0
- package/dist/tools/ask-choice/question.d.ts +16 -0
- package/dist/tools/ask-choice/question.js +60 -0
- package/dist/tools/ask-choice/schema.d.ts +91 -0
- package/dist/tools/ask-choice/schema.js +27 -0
- package/dist/tools/ask-choice/types.d.ts +106 -0
- package/dist/tools/ask-choice/types.js +2 -0
- package/package.json +43 -5
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { FederationMessages } from "./messages.js";
|
|
2
|
+
import { type ToolExecutionContext, type ToolExecutionOptions, type ToolRegistration } from "@jini-ai/core";
|
|
3
|
+
import { McpAuthFailedError } from "./mcp-protocol.js";
|
|
4
|
+
import type { FederatedCallConfirmationOutcome, FederatedCallConfirmationRequest, FederatedCallTarget, FederatedMcpConnectionConfig, McpSessionPort } from "./ports.js";
|
|
5
|
+
import { admitRemoteTools, type FederatedAdmissionReport } from "./trust.js";
|
|
6
|
+
/**
|
|
7
|
+
* @file Turns one connected external MCP server into `ToolRegistration`s — the federated
|
|
8
|
+
* counterpart of a domain's `tool-registrations.ts`, and deliberately NOT one of them.
|
|
9
|
+
*
|
|
10
|
+
* The structural point of this file is what it does not do. It does not call
|
|
11
|
+
* `buildDomainRegistrations`, it does not appear in `assistant/tool-registrations.ts`'s
|
|
12
|
+
* `DOMAIN_SLICES`, and it contributes nothing to `DERIVED_RISK_BY_TOOL_ID`. Those three mechanisms
|
|
13
|
+
* are the native tier's guarantees — a reviewed static catalog, an independently-derived risk
|
|
14
|
+
* classification, a build that fails on any entry that is neither wired nor explicitly excused —
|
|
15
|
+
* and a runtime-discovered third-party tool can satisfy none of them. Routing federated tools
|
|
16
|
+
* through them would not extend those guarantees to federated tools; it would only stop them
|
|
17
|
+
* meaning anything for the native ones. So federation gets its own construction path, with its own
|
|
18
|
+
* gates (`trust.ts`), and the two surfaces stay separable by id, by permission, and by audit source.
|
|
19
|
+
*
|
|
20
|
+
* They do converge in exactly one place, and it is unavoidable: the single `ToolRegistry` the
|
|
21
|
+
* daemon's `ToolExecutor` executes against (`agent-daemon-server.ts`). A tool the executor cannot
|
|
22
|
+
* see cannot be called. Convergence there is mechanical, and everything that makes the tiers
|
|
23
|
+
* distinct — namespacing, the operator allowlist, the one-way hint rule, the untrusted-data
|
|
24
|
+
* envelope, the coarse `admin.integrations.manage` gate — is applied on this side of it, before
|
|
25
|
+
* anything is handed over. The audit trail stays separable for free: `tool-executor-audit.ts` wraps
|
|
26
|
+
* the executor, so federated attempts are recorded like any other, distinguishable by their
|
|
27
|
+
* `mcp__` id prefix.
|
|
28
|
+
*
|
|
29
|
+
* Authorization shape, in this file's own terms (the question every domain file answers): the
|
|
30
|
+
* remote has no `authorize` call of its own and never will, so the handler here performs the
|
|
31
|
+
* check itself via the host's `permissionGate` — the same shape as
|
|
32
|
+
* `features/database/tool-registrations.ts`'s read handlers, which gate inline because their domain
|
|
33
|
+
* functions carry no gate to inherit. It is one evaluator, not two.
|
|
34
|
+
* See docs/decisions/DR-003-trusted-federation-composition.md.
|
|
35
|
+
*/
|
|
36
|
+
/** What `federate` needs from a composition root. A narrow slice of `RouteDeps`, deliberately —
|
|
37
|
+
* federation touches no repo, no clock, no id generator, and asking for the whole bag would imply
|
|
38
|
+
* otherwise. */
|
|
39
|
+
/** The host's one permission evaluator. Throws to deny; checked before a remote sees arguments.
|
|
40
|
+
* @complexity Determined by the injected evaluator.
|
|
41
|
+
*/
|
|
42
|
+
export type FederatedToolPermissionGate = (request: {
|
|
43
|
+
readonly context: ToolExecutionContext;
|
|
44
|
+
readonly permission: string;
|
|
45
|
+
readonly entityType: string;
|
|
46
|
+
readonly entityId: string;
|
|
47
|
+
}, optional?: {
|
|
48
|
+
scope?: string;
|
|
49
|
+
}) => void | Promise<void>;
|
|
50
|
+
export interface FederationDeps {
|
|
51
|
+
readonly permissionGate: FederatedToolPermissionGate;
|
|
52
|
+
readonly messages: FederationMessages;
|
|
53
|
+
readonly errorCode: string;
|
|
54
|
+
readonly scope?: string;
|
|
55
|
+
/**
|
|
56
|
+
* A LIVENESS gate, checked before every federated call. Optional; when absent, behaviour is
|
|
57
|
+
* exactly what it was before this existed.
|
|
58
|
+
*
|
|
59
|
+
* ## Why a gate rather than unregistering the tools
|
|
60
|
+
*
|
|
61
|
+
* Nothing in this subtree unregisters anything, and that is structural rather than an omission:
|
|
62
|
+
* `buildToolCatalogQuery` snapshots `registry.list({})` into a ONE-SHOT FTS index at boot, so a tool
|
|
63
|
+
* removed from the registry afterwards would still be discoverable by `search_tools` and
|
|
64
|
+
* `describe_tool` while no longer being executable — strictly worse than leaving it registered.
|
|
65
|
+
* `trust.ts` R5's "frozen at connect" guarantee rests on the same snapshot.
|
|
66
|
+
*
|
|
67
|
+
* So a connection that dies mid-run cannot be taken out of the catalog. What it CAN do is refuse
|
|
68
|
+
* at the call, with an error the model can act on. Without that, the failure surfaces as
|
|
69
|
+
* `mcp-federation: session is closed (<reason>)` — which reads like a transient fault, so the
|
|
70
|
+
* model searches, selects the same tool, fails, and searches again, burning the rest of the run on
|
|
71
|
+
* a server that cannot work until a human re-authorizes it.
|
|
72
|
+
*
|
|
73
|
+
* The gate is checked BEFORE the permission check for one reason: "this server is disconnected"
|
|
74
|
+
* is true regardless of who is asking, and reporting a permission failure to a principal who does
|
|
75
|
+
* have the permission would send them looking in the wrong place.
|
|
76
|
+
*
|
|
77
|
+
* This is ALSO where operator REVOCATION is enforced, not just liveness: a connection deleted,
|
|
78
|
+
* disabled, narrowed (allowlist or write list), or otherwise changed since admission has no way to
|
|
79
|
+
* be un-registered (see this doc's own paragraph above), so every one of those must be caught here
|
|
80
|
+
* instead, on every call, by re-reading the connection's current row.
|
|
81
|
+
* `assistant/external-mcp-oauth.ts`'s `createExternalMcpConnectionGate` is the composition root
|
|
82
|
+
* that does both jobs — the original liveness check plus this revocation re-check
|
|
83
|
+
* (`assistant/external-mcp-revocation.ts`'s `rosterRefusalFor`) — and it is NOT a `ToolPolicy` deny:
|
|
84
|
+
* see that gate's own doc for why a handler-side refusal, not a policy one, is what lets the model
|
|
85
|
+
* see WHY rather than a fixed "denied by policy" string.
|
|
86
|
+
*
|
|
87
|
+
* @param call - The tool's identity as admitted (remote name, declared annotations) plus the
|
|
88
|
+
* connection's origin, so the gate can tell a preset (no row to compare against) from a roster
|
|
89
|
+
* connection (re-checked against its current row every call).
|
|
90
|
+
* @throws Whatever the composition root's terminal error is — for OAuth connections,
|
|
91
|
+
* `assistant/external-mcp-oauth.ts`'s `ExternalMcpReauthRequiredError`, whose message tells the
|
|
92
|
+
* model in words not to retry. For a revoked roster connection,
|
|
93
|
+
* `assistant/external-mcp-revocation.ts`'s `ExternalMcpConnectionRevokedError`.
|
|
94
|
+
*/
|
|
95
|
+
readonly assertConnectionUsable?: ((required: {
|
|
96
|
+
connectionId: string;
|
|
97
|
+
call: FederatedCallTarget;
|
|
98
|
+
}) => void | Promise<void>) | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Called when a live call throws {@link McpAuthFailedError} — the remote itself rejected our
|
|
101
|
+
* authentication (HTTP 401), discovered mid-session rather than at boot. HTTP 403 is a call-level
|
|
102
|
+
* authorization refusal and does not invalidate credentials. Optional; when absent,
|
|
103
|
+
* the `McpAuthFailedError` propagates unchanged, which is a `McpProtocolError` and reads to a
|
|
104
|
+
* model like any other transient transport fault (see `assertConnectionUsable` above for why that
|
|
105
|
+
* shape invites a retry loop).
|
|
106
|
+
*
|
|
107
|
+
* A composition root that KNOWS what "authorization" means for this connection (an OAuth-backed
|
|
108
|
+
* row, today — `assistant/external-mcp-oauth.ts`) wires this to record the durable state — so
|
|
109
|
+
* `assertConnectionUsable`'s cheap row read catches the NEXT call instead of this module reaching
|
|
110
|
+
* the network again — and to throw the same terminal, non-retryable error that path already uses.
|
|
111
|
+
* Always throws; this module does not decide what replaces the original error.
|
|
112
|
+
*/
|
|
113
|
+
readonly onAuthFailed?: ((required: {
|
|
114
|
+
connectionId: string;
|
|
115
|
+
error: McpAuthFailedError;
|
|
116
|
+
}) => Promise<never>) | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* G3 (`trust.ts` R3): asks a human to Confirm or Cancel ONE call to a tool that is not marked
|
|
119
|
+
* read-only, before anything reaches the remote. Wired by the composition root to the held-open
|
|
120
|
+
* MCP-UI card (`assistant/external-mcp-call-confirmation.ts`).
|
|
121
|
+
*
|
|
122
|
+
* Optional only so a root with no human in the loop still type-checks: when it is absent, every
|
|
123
|
+
* tool whose confirmation is not `"none"` is refused at the call and nothing is sent — fail closed,
|
|
124
|
+
* never "run it anyway".
|
|
125
|
+
*/
|
|
126
|
+
readonly confirmCall?: ((required: {
|
|
127
|
+
context: ToolExecutionContext & ToolExecutionOptions;
|
|
128
|
+
request: FederatedCallConfirmationRequest;
|
|
129
|
+
}) => Promise<FederatedCallConfirmationOutcome>) | undefined;
|
|
130
|
+
}
|
|
131
|
+
export interface FederatedRegistrationResult {
|
|
132
|
+
readonly registrations: ToolRegistration[];
|
|
133
|
+
/** The full accounting of what was admitted and refused, for the caller to log. */
|
|
134
|
+
readonly report: FederatedAdmissionReport;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Builds the `ToolRegistration`s for one federated connection from its already-listed tool surface.
|
|
138
|
+
*
|
|
139
|
+
* Split from the connect/list I/O on purpose: this function is pure given a session, so every trust
|
|
140
|
+
* rule and every handler behaviour is testable without a transport.
|
|
141
|
+
*
|
|
142
|
+
* @param params.tools - The remote's advertised surface, verbatim and untrusted.
|
|
143
|
+
* @param params.session - The live session, used only to forward `tools/call`.
|
|
144
|
+
* @param params.config - The site owner's connection config — the trusted side of admission.
|
|
145
|
+
* @param params.nativeToolIds - Every id already registered natively, for the R1 collision assertion.
|
|
146
|
+
* @throws {Error} If a federated id would collide with a native one, or the connection id is invalid.
|
|
147
|
+
* @complexity O(t) in the advertised tool count.
|
|
148
|
+
*/
|
|
149
|
+
export declare function buildFederatedMcpRegistrations(params: {
|
|
150
|
+
tools: Parameters<typeof admitRemoteTools>[0]["tools"];
|
|
151
|
+
session: McpSessionPort;
|
|
152
|
+
config: FederatedMcpConnectionConfig;
|
|
153
|
+
deps: FederationDeps;
|
|
154
|
+
nativeToolIds: ReadonlySet<string>;
|
|
155
|
+
}): FederatedRegistrationResult;
|
|
156
|
+
/**
|
|
157
|
+
* Connects nothing and lists nothing itself — takes a session, drains its tool list, and returns
|
|
158
|
+
* registrations. The one place `listTools` is called, so `trust.ts` R5's "frozen at connect" is a
|
|
159
|
+
* property of the code rather than a convention: there is no other path that could re-list.
|
|
160
|
+
*
|
|
161
|
+
* @throws {Error} If `listTools` rejects — a connection that cannot enumerate is not usable, and
|
|
162
|
+
* `bootstrap.ts` is where that is turned into "carry on without federation".
|
|
163
|
+
* @complexity O(t) in the advertised tool count.
|
|
164
|
+
*/
|
|
165
|
+
export declare function federateSession(params: {
|
|
166
|
+
session: McpSessionPort;
|
|
167
|
+
config: FederatedMcpConnectionConfig;
|
|
168
|
+
deps: FederationDeps;
|
|
169
|
+
nativeToolIds: ReadonlySet<string>;
|
|
170
|
+
}): Promise<FederatedRegistrationResult>;
|
|
171
|
+
//# sourceMappingURL=registrations.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ToolInputError } from "@jini-ai/core";
|
|
2
|
+
import { McpAuthFailedError } from "./mcp-protocol.js";
|
|
3
|
+
import { admitRemoteTools, isOperatorDeclaredReadOnly, assertNoNativeCollision, extractFederatedImageBlocks, FEDERATED_ENTITY_TYPE, FEDERATED_TOOL_PERMISSION, wrapUntrustedResult, writeShapedInputNames, federatedCallConfirmationForAction, } from "./trust.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the `ToolRegistration`s for one federated connection from its already-listed tool surface.
|
|
6
|
+
*
|
|
7
|
+
* Split from the connect/list I/O on purpose: this function is pure given a session, so every trust
|
|
8
|
+
* rule and every handler behaviour is testable without a transport.
|
|
9
|
+
*
|
|
10
|
+
* @param params.tools - The remote's advertised surface, verbatim and untrusted.
|
|
11
|
+
* @param params.session - The live session, used only to forward `tools/call`.
|
|
12
|
+
* @param params.config - The site owner's connection config — the trusted side of admission.
|
|
13
|
+
* @param params.nativeToolIds - Every id already registered natively, for the R1 collision assertion.
|
|
14
|
+
* @throws {Error} If a federated id would collide with a native one, or the connection id is invalid.
|
|
15
|
+
* @complexity O(t) in the advertised tool count.
|
|
16
|
+
*/
|
|
17
|
+
export function buildFederatedMcpRegistrations(params) {
|
|
18
|
+
const { session, config, deps } = params;
|
|
19
|
+
const report = admitRemoteTools({ tools: params.tools, config });
|
|
20
|
+
assertNoNativeCollision({ federatedToolIds: report.admitted.map((tool) => tool.toolId), nativeToolIds: params.nativeToolIds, messages: deps.messages });
|
|
21
|
+
const registrations = report.admitted.map((tool) => {
|
|
22
|
+
const handler = async (ctx, options = {}) => {
|
|
23
|
+
// Liveness AND revocation first — see `FederationDeps.assertConnectionUsable` for why this
|
|
24
|
+
// precedes the permission check rather than following it.
|
|
25
|
+
await deps.assertConnectionUsable?.({ connectionId: config.connectionId, call: {
|
|
26
|
+
remoteName: tool.remoteName,
|
|
27
|
+
declaredAnnotations: tool.declaredAnnotations,
|
|
28
|
+
origin: config.origin,
|
|
29
|
+
} });
|
|
30
|
+
// ONE evaluator, run before anything crosses the network — not after, so a denied principal's
|
|
31
|
+
// arguments are never even sent to a third party. `entityId` is the connection, so a
|
|
32
|
+
// deployment can grant per-connection rather than all-or-nothing.
|
|
33
|
+
await deps.permissionGate({
|
|
34
|
+
context: ctx,
|
|
35
|
+
permission: FEDERATED_TOOL_PERMISSION,
|
|
36
|
+
entityType: FEDERATED_ENTITY_TYPE,
|
|
37
|
+
entityId: config.connectionId,
|
|
38
|
+
}, deps.scope === undefined ? {} : { scope: deps.scope });
|
|
39
|
+
// Fixed ONCE, before any card is drawn: the frozen copy the human sees is the same object that
|
|
40
|
+
// is sent, so nothing that happens to `ctx.input` while the card waits can change what runs.
|
|
41
|
+
const args = frozenArguments(ctx.input);
|
|
42
|
+
const declined = await askBeforeCall({ ...ctx, ...options }, deps, config, tool, args);
|
|
43
|
+
if (declined)
|
|
44
|
+
return declined;
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
result = await session.callTool({ name: tool.remoteName, arguments: args }, { signal: ctx.signal });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// A token valid at boot can die mid-session; nothing here re-probes it proactively (no
|
|
51
|
+
// periodic refresh exists), so this is where that discovery actually happens. Handed to
|
|
52
|
+
// `onAuthFailed` so the SAME durable state and terminal, non-retryable error this file's
|
|
53
|
+
// `assertConnectionUsable` doc already promises apply here too — not just to a connection
|
|
54
|
+
// already known dead at the call's start.
|
|
55
|
+
if (error instanceof McpAuthFailedError && deps.onAuthFailed)
|
|
56
|
+
await deps.onAuthFailed({ connectionId: config.connectionId, error: error });
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
// R7's media carve-out (trust.ts): image blocks are pulled out of `result.content` BEFORE the
|
|
60
|
+
// untrusted-data envelope is built, so they reach the model through the daemon's typed
|
|
61
|
+
// `media` channel (`extractResultMedia`) intact — see `extractFederatedImageBlocks`'s own doc
|
|
62
|
+
// for why stringifying them into the byte-capped text boundary instead would corrupt them.
|
|
63
|
+
const { images, remainder } = extractFederatedImageBlocks({
|
|
64
|
+
content: result.content,
|
|
65
|
+
maxResultBytes: config.maxResultBytes,
|
|
66
|
+
});
|
|
67
|
+
// R7. Every federated result reaches the model inside an untrusted-data boundary, including
|
|
68
|
+
// the remote's own `isError` claim — which is reported as data rather than acted on, because
|
|
69
|
+
// a remote lying about its own success is not a case this side can adjudicate.
|
|
70
|
+
return {
|
|
71
|
+
federated: { connectionId: config.connectionId, tool: tool.remoteName, remoteReportedError: result.isError === true },
|
|
72
|
+
untrusted: wrapUntrustedResult({
|
|
73
|
+
connectionLabel: config.label,
|
|
74
|
+
remoteName: tool.remoteName,
|
|
75
|
+
result: { content: remainder, structuredContent: result.structuredContent },
|
|
76
|
+
maxResultBytes: config.maxResultBytes,
|
|
77
|
+
}),
|
|
78
|
+
// The ONLY field `@jini-ai/daemon`'s `extractResultMedia` reads to hoist inline media onto
|
|
79
|
+
// the `tool_result` wire event — see `demo-image-tool.ts` for the identical shape proven
|
|
80
|
+
// end to end through the chat pane. Omitted (not an empty array) when there is nothing to
|
|
81
|
+
// hoist, so a text-only result's return shape is byte-identical to before this existed.
|
|
82
|
+
...(images.length > 0 ? { content: images } : {}),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
descriptor: {
|
|
87
|
+
id: tool.toolId,
|
|
88
|
+
description: tool.description,
|
|
89
|
+
inputSchema: tool.inputSchema,
|
|
90
|
+
...(isOperatorDeclaredReadOnly({ remoteName: tool.remoteName, annotations: tool.declaredAnnotations, readList: config.readOnlyRemoteNames }) ? { readOnly: true } : {}),
|
|
91
|
+
},
|
|
92
|
+
// Pass-through, matching `buildDomainRegistrations`'s identical choice and for the identical
|
|
93
|
+
// §2 reason: the handler above IS this tool's one gate, and a `ToolPolicy` check would. See docs/decisions/DR-003-trusted-federation-composition.md.
|
|
94
|
+
// be a second evaluator of the same rule.
|
|
95
|
+
policy: { authorize: () => "allow" },
|
|
96
|
+
handler,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return { registrations, report };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Connects nothing and lists nothing itself — takes a session, drains its tool list, and returns
|
|
103
|
+
* registrations. The one place `listTools` is called, so `trust.ts` R5's "frozen at connect" is a
|
|
104
|
+
* property of the code rather than a convention: there is no other path that could re-list.
|
|
105
|
+
*
|
|
106
|
+
* @throws {Error} If `listTools` rejects — a connection that cannot enumerate is not usable, and
|
|
107
|
+
* `bootstrap.ts` is where that is turned into "carry on without federation".
|
|
108
|
+
* @complexity O(t) in the advertised tool count.
|
|
109
|
+
*/
|
|
110
|
+
export async function federateSession(params) {
|
|
111
|
+
const tools = await params.session.listTools();
|
|
112
|
+
return buildFederatedMcpRegistrations({ ...params, tools });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* G3: the per-call human gate for protected actions (`trust.ts` R3): permanent deletion, delivery
|
|
116
|
+
* to people, and changes to assistant privacy/instructions/access. Write-shaped input names
|
|
117
|
+
* (`WRITE_SHAPED_INPUT_WORDS`) describe required cards without independently requiring one. Returns
|
|
118
|
+
* `null` when the call may proceed — an ordinary action, or an explicit Confirm — and otherwise the
|
|
119
|
+
* model-facing result that replaces the call. One card per call: the card is opened here, inside the
|
|
120
|
+
* call it guards, and closes when answered, so one Confirm authorizes exactly one call.
|
|
121
|
+
*
|
|
122
|
+
* @throws {ToolInputError} `${errorCode}_NO_CONFIRMATION_CHANNEL` when the root wired no confirmer —
|
|
123
|
+
* nothing is sent.
|
|
124
|
+
* @complexity O(1) plus the wait for the human.
|
|
125
|
+
*/
|
|
126
|
+
async function askBeforeCall(ctx, deps, config, tool, args) {
|
|
127
|
+
const confirmation = federatedCallConfirmationForAction({ remoteName: tool.remoteName, annotations: tool.declaredAnnotations, args: args });
|
|
128
|
+
if (confirmation === "none")
|
|
129
|
+
return null;
|
|
130
|
+
const writeShapedInputs = [...new Set([...tool.writeShapedInputs, ...writeShapedInputNames({ args: args })])].sort();
|
|
131
|
+
if (!deps.confirmCall) {
|
|
132
|
+
throw new ToolInputError({ message: `${deps.errorCode}_NO_CONFIRMATION_CHANNEL: ${tool.toolId}: this protected action requires confirmation, ` +
|
|
133
|
+
"and nothing here can ask a person. Nothing was sent." });
|
|
134
|
+
}
|
|
135
|
+
const outcome = await deps.confirmCall({ context: ctx, request: {
|
|
136
|
+
toolId: tool.toolId,
|
|
137
|
+
remoteName: tool.remoteName,
|
|
138
|
+
connectionId: config.connectionId,
|
|
139
|
+
connectionLabel: config.label,
|
|
140
|
+
arguments: args,
|
|
141
|
+
destructive: confirmation === "confirm-destructive",
|
|
142
|
+
declaredAnnotations: tool.declaredAnnotations,
|
|
143
|
+
origin: config.origin,
|
|
144
|
+
description: tool.description,
|
|
145
|
+
inputSchema: tool.inputSchema,
|
|
146
|
+
writeShapedInputs,
|
|
147
|
+
} });
|
|
148
|
+
if (outcome.confirmed)
|
|
149
|
+
return null;
|
|
150
|
+
return { federated: { connectionId: config.connectionId, tool: tool.remoteName }, ran: false, ...outcome.result };
|
|
151
|
+
}
|
|
152
|
+
/** {@link normalizeArguments}, then a deep, frozen copy — the one object both shown and sent (G3). */
|
|
153
|
+
function frozenArguments(input) {
|
|
154
|
+
return deepFreeze(structuredClone(normalizeArguments(input)));
|
|
155
|
+
}
|
|
156
|
+
function deepFreeze(value) {
|
|
157
|
+
if (value !== null && typeof value === "object") {
|
|
158
|
+
for (const child of Object.values(value))
|
|
159
|
+
deepFreeze(child);
|
|
160
|
+
Object.freeze(value);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Narrows `ToolExecutionContext.input` to the `arguments` object a `tools/call` carries.
|
|
166
|
+
*
|
|
167
|
+
* Undefined and `{}` both become `{}` — MCP servers routinely publish parameterless tools, and the
|
|
168
|
+
* kit's own `requireNoInput` establishes that "omit it or pass `{}`" is this codebase's convention
|
|
169
|
+
* for one. Anything else is refused rather than coerced: forwarding an array or a string as
|
|
170
|
+
* `arguments` would produce a remote-side error the model cannot act on, and silently dropping it
|
|
171
|
+
* would teach the model its argument was accepted.
|
|
172
|
+
*/
|
|
173
|
+
function normalizeArguments(input) {
|
|
174
|
+
if (input === undefined || input === null)
|
|
175
|
+
return {};
|
|
176
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
177
|
+
throw new Error("input must be an object — federated MCP tools take a JSON object of arguments, or omit input entirely");
|
|
178
|
+
}
|
|
179
|
+
return input;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=registrations.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Logger } from "@jini-ai/core/primitives";
|
|
2
|
+
import { attachFederatedMcpTools, type AttachFederatedMcpToolsParams, type AttachFederatedToolsResult } from "./bootstrap.js";
|
|
3
|
+
import type { ResolvedFederatedConnection } from "./config.js";
|
|
4
|
+
import type { FederationDeps } from "./registrations.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file Hot re-admission of federated MCP connections — the piece that lets an owner who finishes an
|
|
7
|
+
* external MCP sign-in (or an operator who saves a ready, non-OAuth connection) see its tools without
|
|
8
|
+
* restarting the daemon. Before this file existed, `agent-daemon-server.ts`'s `start()` called
|
|
9
|
+
* `attachFederatedMcpTools` exactly once, at boot; a connection that did not exist (or was not yet
|
|
10
|
+
* authorized) at that moment contributed no tools until the whole process restarted — see that file's
|
|
11
|
+
* `registerFederationReloadRoute` call site for how an operator-authorized event now reaches here.
|
|
12
|
+
*
|
|
13
|
+
* ## What this does NOT do, on purpose
|
|
14
|
+
*
|
|
15
|
+
* `trust.ts` R5 freezes an ALREADY-ADMITTED connection's tool set at connect, forever, for this
|
|
16
|
+
* process's lifetime — the rug-pull protection that file's own header explains. This module never
|
|
17
|
+
* re-lists, re-classifies, or re-admits a connectionId that has already gone through
|
|
18
|
+
* {@link attachFederatedMcpTools} once, whether that pass ran at boot or at an earlier reload
|
|
19
|
+
* ({@link selectUnadmittedConnections} is the whole enforcement mechanism: a connectionId already in
|
|
20
|
+
* `admittedConnectionIds` is filtered out before `attach` is ever called on it again). It only ever
|
|
21
|
+
* widens the set of connections that have been admitted AT ALL — new rows, never revised ones — which
|
|
22
|
+
* is the same guarantee a fresh boot already gives, just applied to a connection that did not exist
|
|
23
|
+
* yet when this process started. `ToolRegistry` (`@jini-ai/core`) is deliberately append-only with no
|
|
24
|
+
* unregister, so this module still has no way to revise or drop an admitted tool even if it wanted to.
|
|
25
|
+
*
|
|
26
|
+
* That admission freeze no longer means an operator's edit is inert until restart, though —
|
|
27
|
+
* `external-mcp-revocation.ts`'s per-call gate (`mcp-federation/registrations.ts`'s
|
|
28
|
+
* `FederationDeps.assertConnectionUsable`) re-reads the connection's row on every call, independent of
|
|
29
|
+
* this reload path. Turning a connection off, deleting it, disconnecting its OAuth, or narrowing its
|
|
30
|
+
* allowlist or write list all take effect on the very NEXT call, with no restart and no reload
|
|
31
|
+
* required. Widening the allowlist, or changing the server's address, command, args or sign-in method,
|
|
32
|
+
* still needs a real restart: the new tools are never registered, and the old ones refuse as
|
|
33
|
+
* `"changed"` (the row's admission revision no longer matches what the tool was admitted under) until
|
|
34
|
+
* the process restarts and re-admits the connection fresh.
|
|
35
|
+
*
|
|
36
|
+
* ## Concurrency
|
|
37
|
+
*
|
|
38
|
+
* {@link createFederationReloadCoordinator} is the single owner of "which connectionIds have been
|
|
39
|
+
* admitted so far" and serializes every reload attempt through one coalesced, trailing-edge
|
|
40
|
+
* single-flight queue — see {@link FederationReloadCoordinator.reload}'s own doc for why a NAIVE
|
|
41
|
+
* single-flight (return the already-in-flight promise to every caller) is wrong here: it can report
|
|
42
|
+
* success to a caller whose own just-committed connection the in-flight pass never saw. Two operator
|
|
43
|
+
* actions landing close together (two OAuth callbacks finishing within the same tick, or a callback
|
|
44
|
+
* racing an admin PUT) can never interleave into a half-updated `ToolRegistry`, and neither can ever
|
|
45
|
+
* cause the append-only registry's "already registered" throw by attempting the same connectionId
|
|
46
|
+
* twice concurrently.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Every roster connection not yet admitted, in roster order. Pure — no I/O — so it is testable in
|
|
50
|
+
* isolation from the I/O `createFederationReloadCoordinator` performs.
|
|
51
|
+
*
|
|
52
|
+
* @complexity O(n) in the roster size.
|
|
53
|
+
*/
|
|
54
|
+
export declare function selectUnadmittedConnections({ allConnections, admittedConnectionIds }: {
|
|
55
|
+
allConnections: readonly ResolvedFederatedConnection[];
|
|
56
|
+
admittedConnectionIds: ReadonlySet<string>;
|
|
57
|
+
}): readonly ResolvedFederatedConnection[];
|
|
58
|
+
export interface FederationReloadResult {
|
|
59
|
+
/** Ids newly admitted by THIS reload pass — empty when nothing in the roster was unadmitted. */
|
|
60
|
+
readonly newlyAdmittedConnectionIds: readonly string[];
|
|
61
|
+
/** This pass's own admission reports only, in `attachFederatedMcpTools`'s own shape — the caller
|
|
62
|
+
* (`agent-daemon-server.ts`) merges these into the running, boot-plus-every-reload accounting
|
|
63
|
+
* `GET /api/federation/admissions` serves; this module holds no history beyond `admittedConnectionIds`. */
|
|
64
|
+
readonly reports: AttachFederatedToolsResult["reports"];
|
|
65
|
+
/** This pass's own connect failures only (2026-09-24) — a connection the reload attempted that
|
|
66
|
+
* never reached admission, same shape and same merge-at-the-caller discipline as `reports`. A
|
|
67
|
+
* connectionId that fails a reload is NOT added to `admittedConnectionIds`, so the next reload
|
|
68
|
+
* attempts it again rather than treating one failed attempt as permanent. */
|
|
69
|
+
readonly connectFailures: AttachFederatedToolsResult["connectFailures"];
|
|
70
|
+
}
|
|
71
|
+
export interface FederationReloadCoordinatorDeps {
|
|
72
|
+
readonly registry: AttachFederatedMcpToolsParams["registry"];
|
|
73
|
+
readonly deps: FederationDeps;
|
|
74
|
+
readonly connect: AttachFederatedMcpToolsParams["connect"];
|
|
75
|
+
/** Reads the operator's current roster fresh. Called once per actual reload PASS — never once per
|
|
76
|
+
* `reload()` caller; see the coalescing doc on {@link FederationReloadCoordinator.reload}. */
|
|
77
|
+
readonly resolveConnections: () => Promise<readonly ResolvedFederatedConnection[]>;
|
|
78
|
+
/** Test seam — defaults to the real {@link attachFederatedMcpTools}. */
|
|
79
|
+
readonly attach?: typeof attachFederatedMcpTools | undefined;
|
|
80
|
+
readonly logger?: Logger | undefined;
|
|
81
|
+
}
|
|
82
|
+
export interface FederationReloadCoordinator {
|
|
83
|
+
/**
|
|
84
|
+
* Re-admits every roster connection not yet admitted.
|
|
85
|
+
*
|
|
86
|
+
* Coalesced trailing-edge single-flight: if a pass is already running, this does NOT hand back
|
|
87
|
+
* that pass's own result (its `resolveConnections()` snapshot may predate whatever made THIS call
|
|
88
|
+
* happen — a just-committed OAuth completion or admin save — so reporting it as this caller's
|
|
89
|
+
* answer could claim success for a connection the running pass never saw), and it does NOT start a
|
|
90
|
+
* second CONCURRENT pass either (two passes racing could both see the same connectionId as
|
|
91
|
+
* unadmitted and both call `registry.register()` for it, tripping the append-only registry's
|
|
92
|
+
* "already registered" throw). Instead it queues exactly one follow-up pass, run after the
|
|
93
|
+
* in-flight one finishes, shared by every caller that arrives while one is already queued — so N
|
|
94
|
+
* callers arriving close together cost at most two real passes: the one already running, plus one
|
|
95
|
+
* guaranteed to read the roster after every one of those callers' own writes committed.
|
|
96
|
+
*/
|
|
97
|
+
reload(required: Record<string, never>): Promise<FederationReloadResult>;
|
|
98
|
+
/** Every connectionId admitted so far, boot-time seed included. A live read of the coordinator's
|
|
99
|
+
* own bookkeeping, not a snapshot — reflects the most recently SETTLED reload pass. */
|
|
100
|
+
admittedConnectionIds(): ReadonlySet<string>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @param initiallyAdmitted - The connectionIds `start()`'s own boot-time `attachFederatedMcpTools`
|
|
104
|
+
* call already admitted — seeded in so the first reload never re-attempts them.
|
|
105
|
+
* @complexity `reload()` is O(c · t) in the unadmitted connection count and their advertised tools
|
|
106
|
+
* (the underlying `attachFederatedMcpTools` cost); `admittedConnectionIds()` is O(1).
|
|
107
|
+
*/
|
|
108
|
+
export declare function createFederationReloadCoordinator({ coordDeps, initiallyAdmitted }: {
|
|
109
|
+
coordDeps: FederationReloadCoordinatorDeps;
|
|
110
|
+
initiallyAdmitted: Iterable<string>;
|
|
111
|
+
}): FederationReloadCoordinator;
|
|
112
|
+
//# sourceMappingURL=reload.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { attachFederatedMcpTools } from "./bootstrap.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file Hot re-admission of federated MCP connections — the piece that lets an owner who finishes an
|
|
4
|
+
* external MCP sign-in (or an operator who saves a ready, non-OAuth connection) see its tools without
|
|
5
|
+
* restarting the daemon. Before this file existed, `agent-daemon-server.ts`'s `start()` called
|
|
6
|
+
* `attachFederatedMcpTools` exactly once, at boot; a connection that did not exist (or was not yet
|
|
7
|
+
* authorized) at that moment contributed no tools until the whole process restarted — see that file's
|
|
8
|
+
* `registerFederationReloadRoute` call site for how an operator-authorized event now reaches here.
|
|
9
|
+
*
|
|
10
|
+
* ## What this does NOT do, on purpose
|
|
11
|
+
*
|
|
12
|
+
* `trust.ts` R5 freezes an ALREADY-ADMITTED connection's tool set at connect, forever, for this
|
|
13
|
+
* process's lifetime — the rug-pull protection that file's own header explains. This module never
|
|
14
|
+
* re-lists, re-classifies, or re-admits a connectionId that has already gone through
|
|
15
|
+
* {@link attachFederatedMcpTools} once, whether that pass ran at boot or at an earlier reload
|
|
16
|
+
* ({@link selectUnadmittedConnections} is the whole enforcement mechanism: a connectionId already in
|
|
17
|
+
* `admittedConnectionIds` is filtered out before `attach` is ever called on it again). It only ever
|
|
18
|
+
* widens the set of connections that have been admitted AT ALL — new rows, never revised ones — which
|
|
19
|
+
* is the same guarantee a fresh boot already gives, just applied to a connection that did not exist
|
|
20
|
+
* yet when this process started. `ToolRegistry` (`@jini-ai/core`) is deliberately append-only with no
|
|
21
|
+
* unregister, so this module still has no way to revise or drop an admitted tool even if it wanted to.
|
|
22
|
+
*
|
|
23
|
+
* That admission freeze no longer means an operator's edit is inert until restart, though —
|
|
24
|
+
* `external-mcp-revocation.ts`'s per-call gate (`mcp-federation/registrations.ts`'s
|
|
25
|
+
* `FederationDeps.assertConnectionUsable`) re-reads the connection's row on every call, independent of
|
|
26
|
+
* this reload path. Turning a connection off, deleting it, disconnecting its OAuth, or narrowing its
|
|
27
|
+
* allowlist or write list all take effect on the very NEXT call, with no restart and no reload
|
|
28
|
+
* required. Widening the allowlist, or changing the server's address, command, args or sign-in method,
|
|
29
|
+
* still needs a real restart: the new tools are never registered, and the old ones refuse as
|
|
30
|
+
* `"changed"` (the row's admission revision no longer matches what the tool was admitted under) until
|
|
31
|
+
* the process restarts and re-admits the connection fresh.
|
|
32
|
+
*
|
|
33
|
+
* ## Concurrency
|
|
34
|
+
*
|
|
35
|
+
* {@link createFederationReloadCoordinator} is the single owner of "which connectionIds have been
|
|
36
|
+
* admitted so far" and serializes every reload attempt through one coalesced, trailing-edge
|
|
37
|
+
* single-flight queue — see {@link FederationReloadCoordinator.reload}'s own doc for why a NAIVE
|
|
38
|
+
* single-flight (return the already-in-flight promise to every caller) is wrong here: it can report
|
|
39
|
+
* success to a caller whose own just-committed connection the in-flight pass never saw. Two operator
|
|
40
|
+
* actions landing close together (two OAuth callbacks finishing within the same tick, or a callback
|
|
41
|
+
* racing an admin PUT) can never interleave into a half-updated `ToolRegistry`, and neither can ever
|
|
42
|
+
* cause the append-only registry's "already registered" throw by attempting the same connectionId
|
|
43
|
+
* twice concurrently.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Every roster connection not yet admitted, in roster order. Pure — no I/O — so it is testable in
|
|
47
|
+
* isolation from the I/O `createFederationReloadCoordinator` performs.
|
|
48
|
+
*
|
|
49
|
+
* @complexity O(n) in the roster size.
|
|
50
|
+
*/
|
|
51
|
+
export function selectUnadmittedConnections({ allConnections, admittedConnectionIds }) {
|
|
52
|
+
return allConnections.filter((connection) => !admittedConnectionIds.has(connection.config.connectionId));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @param initiallyAdmitted - The connectionIds `start()`'s own boot-time `attachFederatedMcpTools`
|
|
56
|
+
* call already admitted — seeded in so the first reload never re-attempts them.
|
|
57
|
+
* @complexity `reload()` is O(c · t) in the unadmitted connection count and their advertised tools
|
|
58
|
+
* (the underlying `attachFederatedMcpTools` cost); `admittedConnectionIds()` is O(1).
|
|
59
|
+
*/
|
|
60
|
+
export function createFederationReloadCoordinator({ coordDeps, initiallyAdmitted }) {
|
|
61
|
+
const admitted = new Set(initiallyAdmitted);
|
|
62
|
+
const attach = coordDeps.attach ?? attachFederatedMcpTools;
|
|
63
|
+
let inFlight = null;
|
|
64
|
+
let queuedNext = null;
|
|
65
|
+
async function runOnePass() {
|
|
66
|
+
const allConnections = await coordDeps.resolveConnections();
|
|
67
|
+
const unadmitted = selectUnadmittedConnections({ allConnections: allConnections, admittedConnectionIds: admitted });
|
|
68
|
+
if (unadmitted.length === 0)
|
|
69
|
+
return { newlyAdmittedConnectionIds: [], reports: [], connectFailures: [] };
|
|
70
|
+
// Empty, not omitted: omission resolves presets from the environment again, even though the
|
|
71
|
+
// boot pass already admitted them. Reload concerns only the operator-editable roster;
|
|
72
|
+
// environment presets cannot change without a real restart.
|
|
73
|
+
const attached = await attach({ registry: coordDeps.registry, deps: coordDeps.deps, connect: coordDeps.connect }, { connections: [], extraConnections: unadmitted, ...(coordDeps.logger ? { logger: coordDeps.logger } : {}) });
|
|
74
|
+
for (const entry of attached.reports)
|
|
75
|
+
admitted.add(entry.connectionId);
|
|
76
|
+
return {
|
|
77
|
+
newlyAdmittedConnectionIds: attached.reports.map((entry) => entry.connectionId),
|
|
78
|
+
reports: attached.reports,
|
|
79
|
+
connectFailures: attached.connectFailures,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function reload(_required) {
|
|
83
|
+
if (!inFlight) {
|
|
84
|
+
inFlight = runOnePass().finally(() => {
|
|
85
|
+
inFlight = null;
|
|
86
|
+
});
|
|
87
|
+
return inFlight;
|
|
88
|
+
}
|
|
89
|
+
if (!queuedNext) {
|
|
90
|
+
// Chained off the CURRENTLY running pass, not reassigned into `inFlight` until it actually
|
|
91
|
+
// starts — a third caller arriving while this follow-up is still only QUEUED (the running pass
|
|
92
|
+
// hasn't settled yet) shares this same queued promise instead of queuing a third pass.
|
|
93
|
+
// `.catch(() => undefined)` first so a broken run (`resolveConnections`/`attach` throwing,
|
|
94
|
+
// possible only via test-injected overrides — the real ones this repo wires are documented
|
|
95
|
+
// "never rejects") cannot wedge every future caller behind a permanently-rejected chain link.
|
|
96
|
+
const runningPass = inFlight;
|
|
97
|
+
queuedNext = runningPass.catch(() => undefined).then(() => {
|
|
98
|
+
const nextPass = runOnePass().finally(() => {
|
|
99
|
+
inFlight = null;
|
|
100
|
+
});
|
|
101
|
+
inFlight = nextPass;
|
|
102
|
+
queuedNext = null;
|
|
103
|
+
return nextPass;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return queuedNext;
|
|
107
|
+
}
|
|
108
|
+
return { reload, admittedConnectionIds: () => admitted };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=reload.js.map
|