@yanlinglabs/winter-agent-runtime 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/embedded-worker.js +6 -4
  2. package/dist/embedded.js +6 -4
  3. package/dist/engine.d.ts +16 -0
  4. package/dist/{index-jz5d90c6.js → index-51673qbw.js} +151 -120
  5. package/dist/{index-gmxxtqpe.js → index-9df1q7j4.js} +30 -12
  6. package/dist/{index-8jgwp1px.js → index-9s68t0pd.js} +170 -11
  7. package/dist/index-bgeqc7ax.js +52 -0
  8. package/dist/{index-eyddzcgs.js → index-wsfqsrh5.js} +2 -2
  9. package/dist/index-z6s3r4xm.js +903 -0
  10. package/dist/index.js +4 -2
  11. package/dist/mcp/client.d.ts +20 -1
  12. package/dist/mcp/lifecycle.d.ts +8 -0
  13. package/dist/mcp/transports/http.d.ts +10 -1
  14. package/dist/mcp/transports/sse.d.ts +4 -2
  15. package/dist/mcp-auth/account.d.ts +33 -0
  16. package/dist/mcp-auth/config.d.ts +6 -0
  17. package/dist/mcp-auth/constants.d.ts +30 -0
  18. package/dist/mcp-auth/discovery.d.ts +72 -0
  19. package/dist/mcp-auth/engine-wiring.d.ts +52 -0
  20. package/dist/mcp-auth/errors.d.ts +37 -0
  21. package/dist/mcp-auth/fetch-policy.d.ts +33 -0
  22. package/dist/mcp-auth/login.d.ts +59 -0
  23. package/dist/mcp-auth/records.d.ts +73 -0
  24. package/dist/mcp-auth/refresh.d.ts +34 -0
  25. package/dist/mcp-auth/revoke.d.ts +15 -0
  26. package/dist/mcp-auth/session-provider.d.ts +55 -0
  27. package/dist/mcp-auth/store.d.ts +85 -0
  28. package/dist/mcp-auth/test-fixture-as.d.ts +72 -0
  29. package/dist/mcp-auth.d.ts +14 -0
  30. package/dist/mcp-auth.js +493 -0
  31. package/dist/mcp-client.d.ts +12 -2
  32. package/dist/mcp-client.js +15 -2
  33. package/dist/production-wiring.d.ts +6 -0
  34. package/dist/provider/host-credentials.d.ts +48 -0
  35. package/dist/provider/keychain-store.d.ts +29 -1
  36. package/dist/provider/session-provider.d.ts +1 -1
  37. package/dist/store/dialect.d.ts +1 -1
  38. package/dist/testing.js +5 -3
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +8 -4
@@ -1,3 +1,11 @@
1
+ import {
2
+ mcpOAuthTokenAccount2,
3
+ MCP_OAUTH_EXPIRY_SKEW_MS2,
4
+ MCP_OAUTH_SESSION_CACHE_MS2,
5
+ readTokenRecordLenient,
6
+ refreshMcpOAuthToken2
7
+ } from "./index-z6s3r4xm.js";
8
+
1
9
  // src/process-groups.ts
2
10
  var live = new Map;
3
11
  var listeners = new Set;
@@ -109,8 +117,117 @@ function toWireElicitContent(content) {
109
117
  return out;
110
118
  }
111
119
 
120
+ // src/mcp-auth/session-provider.ts
121
+ class McpNeedsAuthError extends Error {
122
+ serverName;
123
+ constructor(serverName, why, hint) {
124
+ super(`MCP server "${serverName}" needs sign-in (${why}). ${hint}`);
125
+ this.name = "McpNeedsAuthError";
126
+ this.serverName = serverName;
127
+ }
128
+ }
129
+
130
+ class McpAuthRefreshUnavailableError extends Error {
131
+ constructor(serverName) {
132
+ super(`MCP server "${serverName}": its sign-in could not be refreshed right now (the authorization server did not answer); reconnect it later`);
133
+ this.name = "McpAuthRefreshUnavailableError";
134
+ }
135
+ }
136
+ function createSessionAuthProvider(opts) {
137
+ const { serverName, oauth } = opts;
138
+ const account = mcpOAuthTokenAccount2(opts.serverUrl);
139
+ const now = oauth.now ?? Date.now;
140
+ let cached;
141
+ let refreshing;
142
+ const needsAuth = (why) => new McpNeedsAuthError(serverName, why, oauth.signInHint(serverName));
143
+ let refreshedGeneration;
144
+ const usable = (record) => {
145
+ if (record.expiresAt === undefined)
146
+ return true;
147
+ const margin = record.refreshToken !== undefined && record.generation !== refreshedGeneration ? MCP_OAUTH_EXPIRY_SKEW_MS2 : 0;
148
+ return record.expiresAt - now() > margin;
149
+ };
150
+ async function read(force) {
151
+ if (!force && cached !== undefined && now() - cached.readAt < MCP_OAUTH_SESSION_CACHE_MS2 && (cached.record === null || usable(cached.record)))
152
+ return cached.record;
153
+ const record = await readTokenRecordLenient(oauth.store, account);
154
+ cached = { record, readAt: now() };
155
+ return record;
156
+ }
157
+ async function ask(generation, stepUpScope) {
158
+ const request = { server: serverName, account, generation, ...stepUpScope !== undefined ? { stepUpScope } : {} };
159
+ if (oauth.askHost !== undefined) {
160
+ const answer = await oauth.askHost(request).catch(() => ({ ok: false, reason: "transient" }));
161
+ if (answer !== "unhandled")
162
+ return answer;
163
+ if (oauth.hostOwnsRefresh === true)
164
+ return { ok: false, reason: "transient" };
165
+ }
166
+ if (oauth.hostOwnsRefresh === true)
167
+ return { ok: false, reason: "transient" };
168
+ const result = await refreshMcpOAuthToken2({ account, store: oauth.store, generation, ...stepUpScope !== undefined ? { stepUpScope } : {}, ...oauth.fetch !== undefined ? { fetch: oauth.fetch } : {}, ...oauth.now !== undefined ? { now: oauth.now } : {} });
169
+ return result.ok ? { ok: true } : result;
170
+ }
171
+ function refresh(generation) {
172
+ refreshing ??= ask(generation).finally(() => {
173
+ refreshing = undefined;
174
+ });
175
+ return refreshing;
176
+ }
177
+ async function refreshed(record) {
178
+ if (record.refreshToken === undefined)
179
+ throw needsAuth("the stored token has expired and there is no refresh token");
180
+ const answer = await refresh(record.generation);
181
+ if (!answer.ok) {
182
+ if (answer.reason === "needs_auth")
183
+ throw needsAuth("the sign-in could not be refreshed");
184
+ throw new McpAuthRefreshUnavailableError(serverName);
185
+ }
186
+ const next = await read(true);
187
+ if (next === null || next.generation === record.generation)
188
+ throw needsAuth("the refreshed sign-in was not found");
189
+ refreshedGeneration = next.generation;
190
+ return next;
191
+ }
192
+ async function current() {
193
+ const record = await read(false);
194
+ if (record === null)
195
+ return;
196
+ if (usable(record))
197
+ return record.accessToken;
198
+ return (await refreshed(record)).accessToken;
199
+ }
200
+ return {
201
+ token: current,
202
+ async preflight() {
203
+ await current();
204
+ },
205
+ async onUnauthorized() {
206
+ const before = cached?.record ?? null;
207
+ const record = await read(true);
208
+ if (record === null)
209
+ throw needsAuth("the server requires a sign-in");
210
+ if (before === null || record.accessToken !== before.accessToken)
211
+ return;
212
+ await refreshed(record);
213
+ },
214
+ async reportInsufficientScope(scope) {
215
+ const record = await read(true).catch(() => null);
216
+ if (record === null || scope === undefined || scope.trim() === "")
217
+ return;
218
+ await ask(record.generation, scope).catch(() => {
219
+ return;
220
+ });
221
+ }
222
+ };
223
+ }
224
+ function mcpSignInHint(brand, serverName) {
225
+ const cli = brand.homeDirName.replace(/^\./, "");
226
+ return `Sign in with \`${cli} mcp login ${serverName}\` (or ${brand.productName} Settings -> MCP), then reconnect the server.`;
227
+ }
228
+
112
229
  // src/mcp/client.ts
113
- import { Client, ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode, SdkHttpError, SseError, UnauthorizedError, UnsupportedProtocolVersionError } from "@modelcontextprotocol/client";
230
+ import { Client, InsufficientScopeError, ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode, SdkHttpError, SseError, UnauthorizedError, UnsupportedProtocolVersionError } from "@modelcontextprotocol/client";
114
231
 
115
232
  // src/mcp/transports/stdio.ts
116
233
  import { spawn } from "node:child_process";
@@ -261,19 +378,22 @@ function buildStdioTransport(cfg, cwd) {
261
378
 
262
379
  // src/mcp/transports/http.ts
263
380
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
381
+ var refusingRedirectsFetch = (url, init) => fetch(url, { ...init, redirect: "error" });
264
382
  function buildHttpTransport(cfg, opts = {}) {
265
383
  const requestInit = { ...cfg.headers !== undefined ? { headers: cfg.headers } : {}, ...opts.refuseRedirects === true ? { redirect: "error" } : {} };
266
384
  const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
267
- ...Object.keys(requestInit).length > 0 ? { requestInit } : {}
385
+ ...Object.keys(requestInit).length > 0 ? { requestInit } : {},
386
+ ...opts.authProvider !== undefined ? { authProvider: opts.authProvider, fetch: refusingRedirectsFetch } : {}
268
387
  });
269
388
  return transport;
270
389
  }
271
390
 
272
391
  // src/mcp/transports/sse.ts
273
392
  import { SSEClientTransport } from "@modelcontextprotocol/client";
274
- function buildSseTransport(cfg) {
393
+ function buildSseTransport(cfg, opts = {}) {
275
394
  const transport = new SSEClientTransport(new URL(cfg.url), {
276
- ...cfg.headers !== undefined ? { requestInit: { headers: cfg.headers } } : {}
395
+ ...cfg.headers !== undefined ? { requestInit: { headers: cfg.headers } } : {},
396
+ ...opts.authProvider !== undefined ? { authProvider: opts.authProvider, fetch: refusingRedirectsFetch } : {}
277
397
  });
278
398
  return transport;
279
399
  }
@@ -310,6 +430,12 @@ function toSdkNegotiationMode(mode) {
310
430
  function classifyConnectError(err, facts = { transportClosed: false, pinned: false }) {
311
431
  if (err instanceof McpConnectError2)
312
432
  return err;
433
+ if (err instanceof McpNeedsAuthError)
434
+ return new McpConnectError2("needs_auth", err.message);
435
+ if (err instanceof McpAuthRefreshUnavailableError)
436
+ return new McpConnectError2("auth_refresh_failed", err.message);
437
+ if (err instanceof InsufficientScopeError)
438
+ return new McpConnectError2("needs_auth", "the server requires additional permission (403 insufficient_scope); sign in again to grant it", 403);
313
439
  if (err instanceof UnauthorizedError)
314
440
  return new McpConnectError2("needs_auth", err.message);
315
441
  if (err instanceof SdkError && err.code === SdkErrorCode.RequestTimeout)
@@ -367,7 +493,7 @@ function autoProbeTimeoutMs(connectTimeoutMs) {
367
493
  return Math.max(1, Math.min(AUTO_PROBE_TIMEOUT_CAP_MS, Math.floor(connectTimeoutMs / 3)));
368
494
  }
369
495
  function retriesAsLegacy(err) {
370
- if (err.code === "needs_auth" || err.code === "spawn_failed" || err.code === "version_mismatch")
496
+ if (err.code === "needs_auth" || err.code === "auth_refresh_failed" || err.code === "spawn_failed" || err.code === "version_mismatch")
371
497
  return false;
372
498
  return err.httpStatus !== 401 && err.httpStatus !== 403;
373
499
  }
@@ -428,10 +554,11 @@ async function connectOnce(opts, mode, connectTimeoutMs, probeTimeoutMs) {
428
554
  throw new McpConnectError2("spawn_failed", `mcp client: server "${name}" is configured as type "sdk" but no in-process server instance was supplied to connectMcpServer`);
429
555
  }
430
556
  transport = await buildSdkTransport(opts.inProcessServer);
431
- } else if (config.type === "http") {
432
- transport = buildHttpTransport(config, opts.refuseHttpRedirects === true ? { refuseRedirects: true } : {});
433
- } else if (config.type === "sse") {
434
- transport = buildSseTransport(config);
557
+ } else if (config.type === "http" || config.type === "sse") {
558
+ if (opts.auth !== undefined)
559
+ await boundedPreflight(opts.auth, connectTimeoutMs);
560
+ const authProvider = opts.auth !== undefined ? { token: () => opts.auth.token(), onUnauthorized: () => opts.auth.onUnauthorized() } : undefined;
561
+ transport = config.type === "http" ? buildHttpTransport(config, { ...opts.refuseHttpRedirects === true ? { refuseRedirects: true } : {}, ...authProvider !== undefined ? { authProvider } : {} }) : buildSseTransport(config, authProvider !== undefined ? { authProvider } : {});
435
562
  } else {
436
563
  if (opts.cwd === undefined) {
437
564
  throw new McpConnectError2("spawn_failed", `mcp client: stdio server "${name}" has no session cwd to start in; it is never started in the host process's cwd`);
@@ -516,10 +643,13 @@ async function connectOnce(opts, mode, connectTimeoutMs, probeTimeoutMs) {
516
643
  }
517
644
  };
518
645
  } catch (err) {
646
+ const scopeChallenge = insufficientScopeOf(err);
647
+ if (scopeChallenge !== undefined && opts.auth !== undefined)
648
+ await opts.auth.reportInsufficientScope(scopeChallenge.requiredScope).catch(() => {});
519
649
  const stderrTail = transport instanceof WinterStdioTransport ? transport.stderrTail.trim() : "";
520
650
  const facts = { transportClosed: transport instanceof WinterStdioTransport && transport.exitedOnItsOwn, pinned: typeof mode === "object" };
521
651
  try {
522
- await transport.close();
652
+ await transport?.close();
523
653
  } catch {}
524
654
  const classified = classifyConnectError(err, facts);
525
655
  if (stderrTail === "" || classified.message.includes(stderrTail))
@@ -529,5 +659,34 @@ async function connectOnce(opts, mode, connectTimeoutMs, probeTimeoutMs) {
529
659
  ${stderrTail}`, classified.httpStatus);
530
660
  }
531
661
  }
662
+ function boundedPreflight(auth, timeoutMs) {
663
+ let timer;
664
+ const outer = new Promise((_, reject) => {
665
+ timer = setTimeout(() => reject(new McpConnectError2("timeout", `mcp client: the sign-in check exceeded ${timeoutMs}ms`)), timeoutMs);
666
+ timer.unref?.();
667
+ });
668
+ return Promise.race([auth.preflight(), outer]).finally(() => {
669
+ if (timer !== undefined)
670
+ clearTimeout(timer);
671
+ });
672
+ }
673
+ function insufficientScopeOf(err) {
674
+ for (let e = err, depth = 0;e !== undefined && e !== null && depth < 4; e = e.cause, depth++) {
675
+ if (e instanceof InsufficientScopeError)
676
+ return e;
677
+ }
678
+ return;
679
+ }
680
+ function classifyMcpAuthFailure(err) {
681
+ for (let e = err, depth = 0;e !== undefined && e !== null && depth < 4; e = e.cause, depth++) {
682
+ if (e instanceof InsufficientScopeError)
683
+ return "insufficient_scope";
684
+ if (e instanceof McpNeedsAuthError || e instanceof UnauthorizedError)
685
+ return "needs_auth";
686
+ if (e instanceof SdkHttpError && e.status === 401)
687
+ return "needs_auth";
688
+ }
689
+ return;
690
+ }
532
691
 
533
- export { trackProcessGroup, liveProcessGroups, onProcessGroupChange, createElicitationAsker2, McpConnectError2, resolveVersionNegotiation2, connectMcpServer2 };
692
+ export { trackProcessGroup, liveProcessGroups, onProcessGroupChange, createElicitationAsker2, createSessionAuthProvider, mcpSignInHint, McpConnectError2, resolveVersionNegotiation2, connectMcpServer2, insufficientScopeOf, classifyMcpAuthFailure };
@@ -0,0 +1,52 @@
1
+ import {
2
+ evaluateMcpAuthUrl2,
3
+ isLoopbackMcpServer
4
+ } from "./index-z6s3r4xm.js";
5
+
6
+ // src/mcp-auth/config.ts
7
+ var OAUTH_KEYS = new Set(["clientId", "clientSecretRef", "callbackPort", "authServerMetadataUrl", "scopes"]);
8
+ var SECRET_REF_KEYS = new Set(["kind"]);
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function validateMcpOAuthConfig2(oauth, serverUrl) {
13
+ if (!isPlainObject(oauth))
14
+ return "MCP server config 'oauth' must be an object";
15
+ for (const key of Object.keys(oauth)) {
16
+ if (key === "clientSecret")
17
+ return `MCP server config 'oauth.clientSecret' is refused: a client secret never lives in a config -- store it in the Keychain (the host's sign-in door) and mark it with 'oauth.clientSecretRef': { kind: "keychain" }`;
18
+ if (!OAUTH_KEYS.has(key))
19
+ return `MCP server config 'oauth' has an unknown key ${JSON.stringify(key)} (expected clientId, clientSecretRef, callbackPort, authServerMetadataUrl, scopes)`;
20
+ }
21
+ const { clientId, clientSecretRef, callbackPort, authServerMetadataUrl, scopes } = oauth;
22
+ if (clientId !== undefined && (typeof clientId !== "string" || clientId.trim() === ""))
23
+ return "MCP server config 'oauth.clientId' must be a non-empty string";
24
+ if (clientSecretRef !== undefined) {
25
+ if (clientId === undefined)
26
+ return "MCP server config 'oauth.clientSecretRef' needs 'oauth.clientId': a secret belongs to a pre-registered client";
27
+ if (!isPlainObject(clientSecretRef))
28
+ return `MCP server config 'oauth.clientSecretRef' must be { kind: "keychain" }`;
29
+ for (const key of Object.keys(clientSecretRef)) {
30
+ if (!SECRET_REF_KEYS.has(key))
31
+ return `MCP server config 'oauth.clientSecretRef' has the key ${JSON.stringify(key)}, which is refused: it is { kind: "keychain" } only -- the secret's Keychain account is derived from the server URL, never named by a config`;
32
+ }
33
+ if (clientSecretRef.kind !== "keychain")
34
+ return `MCP server config 'oauth.clientSecretRef.kind' must be "keychain"`;
35
+ }
36
+ if (callbackPort !== undefined && (typeof callbackPort !== "number" || !Number.isInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535)) {
37
+ return "MCP server config 'oauth.callbackPort' must be an integer port (1-65535)";
38
+ }
39
+ if (authServerMetadataUrl !== undefined) {
40
+ if (typeof authServerMetadataUrl !== "string")
41
+ return "MCP server config 'oauth.authServerMetadataUrl' must be a URL string";
42
+ const verdict = evaluateMcpAuthUrl2(authServerMetadataUrl, { allowLoopback: serverUrl !== undefined && isLoopbackMcpServer(serverUrl) });
43
+ if (!verdict.ok)
44
+ return `MCP server config 'oauth.authServerMetadataUrl' is refused: ${verdict.reason}`;
45
+ }
46
+ if (scopes !== undefined && (!Array.isArray(scopes) || scopes.some((s) => typeof s !== "string" || s === "" || /\s/.test(s)))) {
47
+ return "MCP server config 'oauth.scopes' must be an array of scope tokens (non-empty, no whitespace)";
48
+ }
49
+ return;
50
+ }
51
+
52
+ export { validateMcpOAuthConfig2 };
@@ -9,14 +9,14 @@ import {
9
9
  resolveEngineSession2,
10
10
  resolveProductionWinterHome2,
11
11
  resolveProductionStoreHome
12
- } from "./index-jz5d90c6.js";
12
+ } from "./index-51673qbw.js";
13
13
  import {
14
14
  registerDefaultChildEngineFactory,
15
15
  loadResumedChain,
16
16
  buildProductionWiring,
17
17
  withAutoSkillPermissions,
18
18
  restoreChildRoster
19
- } from "./index-gmxxtqpe.js";
19
+ } from "./index-9df1q7j4.js";
20
20
 
21
21
  // src/embedded.ts
22
22
  import { splitFrames, encodeFrame } from "@yanlinglabs/winter-agent-sdk";