@mysten-incubation/memwal-mcp 0.0.11 → 0.0.12-dev.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/dist/auth-required.d.ts.map +1 -1
- package/dist/auth-required.js +46 -9
- package/dist/auth-required.js.map +1 -1
- package/dist/auth.d.ts +3 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +242 -25
- package/dist/bridge.js.map +1 -1
- package/dist/client-info.d.ts +24 -0
- package/dist/client-info.d.ts.map +1 -0
- package/dist/client-info.js +54 -0
- package/dist/client-info.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/login.d.ts +9 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +28 -2
- package/dist/login.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { clearCreds, credsPath } from "./auth.js";
|
|
2
2
|
import { TOOL_DEFINITIONS } from "./auth-required.js";
|
|
3
|
+
import { clientInfoHeaders, lastClientInfoHeaders, rememberInitializeClientInfo, } from "./client-info.js";
|
|
3
4
|
import { ensureCompatibleRelayer, resolveConnectTimeoutMs } from "./compatibility.js";
|
|
4
5
|
import { PROACTIVE_INSTRUCTIONS } from "./instructions.js";
|
|
5
|
-
import { startOrReuseLoginFlow } from "./login.js";
|
|
6
|
+
import { startOrReuseLoginFlow, resolveLoginTimeoutMs } from "./login.js";
|
|
6
7
|
import { log, note } from "./logger.js";
|
|
7
8
|
import { MEMWAL_MCP_VERSION } from "./version.js";
|
|
8
9
|
/** Memory tools that take a `namespace` argument. `memwal_remember`,
|
|
@@ -61,7 +62,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
61
62
|
},
|
|
62
63
|
{
|
|
63
64
|
name: "memwal_logout",
|
|
64
|
-
description: "
|
|
65
|
+
description: "Sign out of Walrus Memory: removes the saved credentials from this machine (~/.memwal/credentials.json) AND closes this connection's memory session, so memory tools stop working until you call memwal_login again. The on-chain delegate key registration is NOT revoked — visit the Walrus Memory dashboard to remove it from your account if needed.",
|
|
65
66
|
inputSchema: {
|
|
66
67
|
type: "object",
|
|
67
68
|
properties: {},
|
|
@@ -103,6 +104,16 @@ function buildLocalInitializeResult(params) {
|
|
|
103
104
|
* imported memory-tool list (which already carries its own `memwal_login`
|
|
104
105
|
* entry) before appending our canonical definitions. */
|
|
105
106
|
const LOCAL_TOOL_NAMES = new Set(LOCAL_TOOL_DEFINITIONS.map((t) => t.name));
|
|
107
|
+
/** Reply for every memory tool call once `memwal_logout` has torn the session
|
|
108
|
+
* down, and for anything still in flight at that moment. Names the way back in
|
|
109
|
+
* so the client isn't left guessing why the tools stopped working. */
|
|
110
|
+
const SIGNED_OUT_TEXT = "❌ Signed out of Walrus Memory. Memory tools are unavailable on this connection until you call `memwal_login` again.";
|
|
111
|
+
/** `failRequest` options for every signed-out refusal, so a request refused at
|
|
112
|
+
* logout time and one refused on arrival afterwards read identically. */
|
|
113
|
+
const SIGNED_OUT_FAILURE = {
|
|
114
|
+
toolText: SIGNED_OUT_TEXT,
|
|
115
|
+
errorMessage: SIGNED_OUT_TEXT,
|
|
116
|
+
};
|
|
106
117
|
/** The `tools/list` we serve LOCALLY at cold start: the memory tools (from the
|
|
107
118
|
* same source as auth-required mode) plus the locally-handled login/logout
|
|
108
119
|
* tools. We strip any locally-served name from the imported list first —
|
|
@@ -120,7 +131,6 @@ const LOCAL_TOOLS_LIST = {
|
|
|
120
131
|
...LOCAL_TOOL_DEFINITIONS,
|
|
121
132
|
],
|
|
122
133
|
};
|
|
123
|
-
const LOGIN_BG_TIMEOUT_MS = 5 * 60_000;
|
|
124
134
|
const URL_READY_TIMEOUT_MS = 5_000;
|
|
125
135
|
/** Maximum silence we tolerate on the SSE stream before assuming the
|
|
126
136
|
* relayer-side session has gone dead. The relayer sends keepalive events
|
|
@@ -163,13 +173,14 @@ function resolveCallTimeoutMs() {
|
|
|
163
173
|
return DEFAULT_CALL_TIMEOUT_MS;
|
|
164
174
|
return n;
|
|
165
175
|
}
|
|
166
|
-
function mcpAuthHeaders(creds) {
|
|
176
|
+
function mcpAuthHeaders(creds, extra = {}) {
|
|
167
177
|
return {
|
|
168
178
|
authorization: `Bearer ${creds.delegatePrivateKey}`,
|
|
169
179
|
"x-memwal-account-id": creds.accountId,
|
|
180
|
+
...extra,
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
|
-
async function openSseStream(relayerUrl, creds) {
|
|
183
|
+
async function openSseStream(relayerUrl, creds, extraHeaders = {}) {
|
|
173
184
|
const connectTimeoutMs = resolveConnectTimeoutMs();
|
|
174
185
|
// One shared budget for the WHOLE attempt: the compatibility check (GET
|
|
175
186
|
// /version + /health fallback) and the SSE connect below both honour this
|
|
@@ -208,7 +219,7 @@ async function openSseStream(relayerUrl, creds) {
|
|
|
208
219
|
resp = await fetch(url, {
|
|
209
220
|
method: "GET",
|
|
210
221
|
headers: {
|
|
211
|
-
...mcpAuthHeaders(creds),
|
|
222
|
+
...mcpAuthHeaders(creds, extraHeaders),
|
|
212
223
|
accept: "text/event-stream",
|
|
213
224
|
"cache-control": "no-cache",
|
|
214
225
|
},
|
|
@@ -411,11 +422,11 @@ async function openSseStream(relayerUrl, creds) {
|
|
|
411
422
|
},
|
|
412
423
|
};
|
|
413
424
|
}
|
|
414
|
-
async function postMessage(postUrl, msg, creds) {
|
|
425
|
+
async function postMessage(postUrl, msg, creds, extraHeaders = {}) {
|
|
415
426
|
const resp = await fetch(postUrl, {
|
|
416
427
|
method: "POST",
|
|
417
428
|
headers: {
|
|
418
|
-
...mcpAuthHeaders(creds),
|
|
429
|
+
...mcpAuthHeaders(creds, extraHeaders),
|
|
419
430
|
"content-type": "application/json",
|
|
420
431
|
},
|
|
421
432
|
body: JSON.stringify(msg),
|
|
@@ -456,7 +467,7 @@ async function handleLocalLogin(config, onCredentials) {
|
|
|
456
467
|
relayerUrl: config.relayerUrl,
|
|
457
468
|
webUrl: config.webUrl,
|
|
458
469
|
label: config.label,
|
|
459
|
-
timeoutMs:
|
|
470
|
+
timeoutMs: resolveLoginTimeoutMs(),
|
|
460
471
|
openBrowser: false,
|
|
461
472
|
}, async (creds) => {
|
|
462
473
|
await onCredentials(creds);
|
|
@@ -464,6 +475,18 @@ async function handleLocalLogin(config, onCredentials) {
|
|
|
464
475
|
accountId: creds.accountId,
|
|
465
476
|
delegateAddress: creds.delegateAddress,
|
|
466
477
|
});
|
|
478
|
+
}, (err) => {
|
|
479
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
480
|
+
log.warn("memwal_login.bridge.failed", { msg });
|
|
481
|
+
writeStdoutMessage({
|
|
482
|
+
jsonrpc: "2.0",
|
|
483
|
+
method: "notifications/message",
|
|
484
|
+
params: {
|
|
485
|
+
level: "warning",
|
|
486
|
+
logger: "memwal-mcp",
|
|
487
|
+
data: `Walrus Memory sign-in did not complete: ${msg}. Existing credentials are unchanged; call memwal_login again to retry.`,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
467
490
|
});
|
|
468
491
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Listener never started")), URL_READY_TIMEOUT_MS).unref?.());
|
|
469
492
|
let url;
|
|
@@ -513,13 +536,19 @@ function handleLocalLogout() {
|
|
|
513
536
|
if (!cleared.removedPath) {
|
|
514
537
|
return {
|
|
515
538
|
isError: false,
|
|
516
|
-
|
|
539
|
+
// The bridge only runs with credentials loaded, so a missing
|
|
540
|
+
// file here still means a live in-memory session to tear down —
|
|
541
|
+
// exactly the GH #616 case. Say so rather than implying nothing
|
|
542
|
+
// happened.
|
|
543
|
+
text: `✅ Already signed out. No credentials at \`${credsPath()}\`, and this ` +
|
|
544
|
+
`connection's memory session has been closed — memory tools will refuse ` +
|
|
545
|
+
`to run until you sign in again.`,
|
|
517
546
|
};
|
|
518
547
|
}
|
|
519
548
|
return {
|
|
520
549
|
isError: false,
|
|
521
550
|
text: [
|
|
522
|
-
`✅ Signed out. Credentials removed from \`${cleared.removedPath}
|
|
551
|
+
`✅ Signed out. Credentials removed from \`${cleared.removedPath}\`, and this connection's memory session has been closed — memory tools will refuse to run until you sign in again.`,
|
|
523
552
|
...(cleared.fallbackPath
|
|
524
553
|
? [
|
|
525
554
|
``,
|
|
@@ -556,18 +585,18 @@ function handleLocalLogout() {
|
|
|
556
585
|
* file directly. They appear in `tools/list` by splicing them into the
|
|
557
586
|
* relayer's response on the way back to the client.
|
|
558
587
|
*/
|
|
559
|
-
export async function runBridge(
|
|
588
|
+
export async function runBridge(initialCreds, config,
|
|
560
589
|
/** Requests the auth-required server already read off stdin before it
|
|
561
590
|
* detected fresh credentials and handed control here (e.g. the
|
|
562
591
|
* `memwal_recall` that triggered the hot-handoff). Replayed once the SSE
|
|
563
592
|
* stream is up so they're served for real instead of being lost in the
|
|
564
593
|
* mode switch — this is what removes the historical "second restart". */
|
|
565
594
|
pendingLines = []) {
|
|
566
|
-
note(`Connecting to ${
|
|
595
|
+
note(`Connecting to ${initialCreds.relayerUrl}...`);
|
|
567
596
|
log.info("bridge.connecting", {
|
|
568
|
-
relayer:
|
|
569
|
-
accountId:
|
|
570
|
-
delegate:
|
|
597
|
+
relayer: initialCreds.relayerUrl,
|
|
598
|
+
accountId: initialCreds.accountId,
|
|
599
|
+
delegate: initialCreds.delegateAddress,
|
|
571
600
|
});
|
|
572
601
|
// Live handle to the current SSE stream — replaced whenever we reconnect.
|
|
573
602
|
// Starts null: we answer `initialize` / `tools/list` LOCALLY and wire stdin
|
|
@@ -576,7 +605,37 @@ pendingLines = []) {
|
|
|
576
605
|
// the background; anything that must reach the relayer (`tools/call`) is
|
|
577
606
|
// buffered in `pendingForward` until `sse` is live, then flushed.
|
|
578
607
|
let sse = null;
|
|
608
|
+
/** The delegate key this bridge is currently authorized to act with.
|
|
609
|
+
* Nulled by `invalidateSession()` so signing out drops the key itself
|
|
610
|
+
* rather than only setting a flag that every forwarding path has to
|
|
611
|
+
* remember to check — after logout there is simply nothing left to sign
|
|
612
|
+
* with. `adoptCredentials` republishes it on the next login. */
|
|
613
|
+
let creds = initialCreds;
|
|
614
|
+
/**
|
|
615
|
+
* Best-effort `x-memwal-client` / `x-memwal-client-version` forwarded to
|
|
616
|
+
* the sidecar. Seeded from `lastClientInfoHeaders()`, which is only
|
|
617
|
+
* populated after the auth-required → bridge handoff (that path does not
|
|
618
|
+
* replay `initialize`). On a normal already-signed-in start this object
|
|
619
|
+
* is empty at first SSE connect: `connectInBackground` opens the stream
|
|
620
|
+
* before stdin is wired. Headers are filled when we see `initialize` and
|
|
621
|
+
* then land on reconnects / later POSTs. The sidecar treats the MCP
|
|
622
|
+
* handshake (`initialize.clientInfo`) as the authoritative source.
|
|
623
|
+
*/
|
|
624
|
+
const extraHeaders = { ...lastClientInfoHeaders() };
|
|
579
625
|
let stdinClosed = false;
|
|
626
|
+
/** Set by `memwal_logout`. Unlike `stdinClosed` the process stays up and
|
|
627
|
+
* the client keeps talking to us — `memwal_login` must still work — but the
|
|
628
|
+
* relayer session is torn down and must never be re-established with the
|
|
629
|
+
* credentials the user just deleted. Every connect/reconnect path therefore
|
|
630
|
+
* checks this alongside `stdinClosed`; `adoptCredentials` clears it when a
|
|
631
|
+
* new login lands. */
|
|
632
|
+
let loggedOut = false;
|
|
633
|
+
/** Resolves once a post-logout `memwal_login` has published a fresh session
|
|
634
|
+
* (or stdin closed). The server pump parks on this instead of exiting, so
|
|
635
|
+
* signing back in resumes streaming without the user restarting their MCP
|
|
636
|
+
* client. Recreated on every logout; null while signed in. */
|
|
637
|
+
let logoutPark = null;
|
|
638
|
+
let releaseLogoutPark = null;
|
|
580
639
|
let reconnectAttempt = 0;
|
|
581
640
|
let reconnectPromise = null;
|
|
582
641
|
let firstConnectDone = false;
|
|
@@ -593,7 +652,7 @@ pendingLines = []) {
|
|
|
593
652
|
function postIfCurrent(epoch, postUrl, msg, postCreds) {
|
|
594
653
|
if (epoch !== sessionEpoch)
|
|
595
654
|
return Promise.resolve(0);
|
|
596
|
-
return postMessage(postUrl, msg, postCreds);
|
|
655
|
+
return postMessage(postUrl, msg, postCreds, extraHeaders);
|
|
597
656
|
}
|
|
598
657
|
let credentialGeneration = 0;
|
|
599
658
|
let activeCredentialGeneration = 0;
|
|
@@ -619,6 +678,10 @@ pendingLines = []) {
|
|
|
619
678
|
if (stdinClosed)
|
|
620
679
|
return;
|
|
621
680
|
stdinClosed = true;
|
|
681
|
+
// Wake a pump parked on logout, or shutdown would block on a login
|
|
682
|
+
// that is never coming.
|
|
683
|
+
releaseLogoutPark?.();
|
|
684
|
+
releaseLogoutPark = null;
|
|
622
685
|
for (const fn of stdinCloseListeners) {
|
|
623
686
|
try {
|
|
624
687
|
fn();
|
|
@@ -713,7 +776,7 @@ pendingLines = []) {
|
|
|
713
776
|
* login credential swap). Credential-generation checks discard a session
|
|
714
777
|
* whose key rotated mid-handshake. */
|
|
715
778
|
async function reconnect(reason, immediate = false) {
|
|
716
|
-
if (stdinClosed)
|
|
779
|
+
if (stdinClosed || loggedOut)
|
|
717
780
|
return;
|
|
718
781
|
if (reconnectPromise)
|
|
719
782
|
return reconnectPromise;
|
|
@@ -751,10 +814,23 @@ pendingLines = []) {
|
|
|
751
814
|
});
|
|
752
815
|
}
|
|
753
816
|
try {
|
|
754
|
-
while (!stdinClosed) {
|
|
817
|
+
while (!stdinClosed && !loggedOut) {
|
|
755
818
|
const openingGeneration = credentialGeneration;
|
|
756
819
|
const openingCreds = creds;
|
|
757
|
-
|
|
820
|
+
// Signed out between the guard above and here: the key is
|
|
821
|
+
// gone, so there is nothing to authorize a new session
|
|
822
|
+
// with. Belt-and-braces against `loggedOut` alone.
|
|
823
|
+
if (!openingCreds)
|
|
824
|
+
break;
|
|
825
|
+
const candidate = await openSseStream(openingCreds.relayerUrl, openingCreds, extraHeaders);
|
|
826
|
+
// Logout can also land mid-handshake. Same reasoning as the
|
|
827
|
+
// stale-credentials case below, except there is no new key
|
|
828
|
+
// to reconnect with — drop the session and stop.
|
|
829
|
+
if (loggedOut) {
|
|
830
|
+
candidate.abort();
|
|
831
|
+
log.info("bridge.reconnect_discarded_signed_out", {});
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
758
834
|
// Login can finish while an older handshake is awaiting its
|
|
759
835
|
// endpoint event. Never publish that stale session: its GET
|
|
760
836
|
// used the old key, while subsequent POSTs would use the new
|
|
@@ -788,6 +864,16 @@ pendingLines = []) {
|
|
|
788
864
|
// and the SSE pump may delete entries concurrently as replies
|
|
789
865
|
// start arriving on the new session.
|
|
790
866
|
for (const [id, entry] of Array.from(inFlight.entries())) {
|
|
867
|
+
// Replay awaits a POST per entry, so a logout can land
|
|
868
|
+
// partway through this loop. The snapshot and
|
|
869
|
+
// `openingCreds` both predate it, so without this the
|
|
870
|
+
// remaining entries would still go out under the key the
|
|
871
|
+
// user just deleted — `invalidateSession` clearing
|
|
872
|
+
// `inFlight` cannot stop a snapshot already taken.
|
|
873
|
+
if (loggedOut || openingGeneration !== credentialGeneration) {
|
|
874
|
+
log.info("bridge.replay_halted_signed_out", { id });
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
791
877
|
const msg = entry.msg;
|
|
792
878
|
try {
|
|
793
879
|
// A replayed `initialize` produces a fresh upstream
|
|
@@ -851,7 +937,10 @@ pendingLines = []) {
|
|
|
851
937
|
}
|
|
852
938
|
}
|
|
853
939
|
async function adoptCredentials(nextCreds) {
|
|
854
|
-
|
|
940
|
+
// `null` after a logout — treated as an account change, which is the
|
|
941
|
+
// safe direction: it purges rather than replays. (`invalidateSession`
|
|
942
|
+
// already emptied both queues, so the purge is a no-op there.)
|
|
943
|
+
const previousAccountId = creds?.accountId ?? null;
|
|
855
944
|
const accountChanged = previousAccountId !== nextCreds.accountId;
|
|
856
945
|
// Never replay an operation authorized for account A against account B.
|
|
857
946
|
// Return explicit retryable errors instead; the caller can decide which
|
|
@@ -915,6 +1004,10 @@ pendingLines = []) {
|
|
|
915
1004
|
creds = nextCreds;
|
|
916
1005
|
credentialGeneration += 1;
|
|
917
1006
|
reconnectAttempt = 0;
|
|
1007
|
+
// Lift the logout halt BEFORE reconnecting — reconnect() refuses to run
|
|
1008
|
+
// while it is set. The parked pump is released further down, once the
|
|
1009
|
+
// new session actually exists.
|
|
1010
|
+
loggedOut = false;
|
|
918
1011
|
log.info("bridge.credentials_updated", {
|
|
919
1012
|
previousAccountId,
|
|
920
1013
|
accountId: creds.accountId,
|
|
@@ -926,6 +1019,56 @@ pendingLines = []) {
|
|
|
926
1019
|
if (activeCredentialGeneration !== credentialGeneration) {
|
|
927
1020
|
await reconnect("login-credentials-generation-mismatch", true);
|
|
928
1021
|
}
|
|
1022
|
+
// Session is live again: wake a pump parked by a previous logout.
|
|
1023
|
+
releaseLogoutPark?.();
|
|
1024
|
+
releaseLogoutPark = null;
|
|
1025
|
+
logoutPark = null;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Tear the relayer session down after a successful `memwal_logout`.
|
|
1029
|
+
*
|
|
1030
|
+
* Deleting the credentials file is not revocation on its own: the bridge
|
|
1031
|
+
* holds the delegate key in memory and owns a live SSE session, so without
|
|
1032
|
+
* this every later memory tool call would still be forwarded and executed
|
|
1033
|
+
* under the key the user just removed (GH #616).
|
|
1034
|
+
*
|
|
1035
|
+
* `loggedOut` is set FIRST so the abort below cannot race the pump into
|
|
1036
|
+
* `reconnect("server-pump-eof")` and immediately re-authorize a new session
|
|
1037
|
+
* with those same in-memory credentials.
|
|
1038
|
+
*/
|
|
1039
|
+
function invalidateSession() {
|
|
1040
|
+
if (loggedOut)
|
|
1041
|
+
return;
|
|
1042
|
+
loggedOut = true;
|
|
1043
|
+
logoutPark = new Promise((resolve) => {
|
|
1044
|
+
releaseLogoutPark = resolve;
|
|
1045
|
+
});
|
|
1046
|
+
try {
|
|
1047
|
+
sse?.abort();
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
/* already dead */
|
|
1051
|
+
}
|
|
1052
|
+
sse = null;
|
|
1053
|
+
// Drop the delegate key itself, not just the flag. Revocation that
|
|
1054
|
+
// rests only on `loggedOut` is one missed check away from forwarding
|
|
1055
|
+
// under the key the user deleted; with `creds` null there is nothing
|
|
1056
|
+
// left to sign with and every forwarding path fails closed instead.
|
|
1057
|
+
creds = null;
|
|
1058
|
+
// Bump the generation so any handshake or replay that captured the old
|
|
1059
|
+
// key before this point discards its work on its next check, exactly as
|
|
1060
|
+
// it would for a mid-flight key rotation.
|
|
1061
|
+
credentialGeneration += 1;
|
|
1062
|
+
// Answer everything still outstanding rather than stranding it: these
|
|
1063
|
+
// were authorized under the old key and must not be replayed later.
|
|
1064
|
+
for (const [, entry] of Array.from(inFlight.entries())) {
|
|
1065
|
+
failRequest(entry.msg, "signed out", SIGNED_OUT_FAILURE);
|
|
1066
|
+
}
|
|
1067
|
+
inFlight.clear();
|
|
1068
|
+
for (const msg of pendingForward.splice(0, pendingForward.length)) {
|
|
1069
|
+
failRequest(msg, "signed out", SIGNED_OUT_FAILURE);
|
|
1070
|
+
}
|
|
1071
|
+
log.info("bridge.session_invalidated", { reason: "logout" });
|
|
929
1072
|
}
|
|
930
1073
|
// Server → client: stream SSE messages to stdout. Loop forever, restart
|
|
931
1074
|
// pump on stream end (which means SSE got cut → we already reconnected).
|
|
@@ -947,8 +1090,23 @@ pendingLines = []) {
|
|
|
947
1090
|
// sibling closure (connectInBackground / reconnect) that TS
|
|
948
1091
|
// analyzes independently. At runtime `sse` is a live handle here.
|
|
949
1092
|
const stream = sse;
|
|
950
|
-
if (!stream)
|
|
1093
|
+
if (!stream) {
|
|
1094
|
+
// Signed out: park rather than exit. Exiting would end the
|
|
1095
|
+
// pump for good, so a later `memwal_login` would reconnect a
|
|
1096
|
+
// session with nothing draining it — the client would hang
|
|
1097
|
+
// instead of recovering. `logoutPark` resolves once the new
|
|
1098
|
+
// session is published (or stdin closes).
|
|
1099
|
+
// Cast for the same reason as `stream` above: every
|
|
1100
|
+
// assignment to `logoutPark` happens in a sibling closure,
|
|
1101
|
+
// so TS narrows it to `null` here. No `!stdinClosed` guard:
|
|
1102
|
+
// the `while` above already established it and nothing is
|
|
1103
|
+
// awaited in between, so it cannot have changed.
|
|
1104
|
+
if (loggedOut) {
|
|
1105
|
+
await logoutPark;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
951
1108
|
break; // stdin closed before we ever connected
|
|
1109
|
+
}
|
|
952
1110
|
while (true) {
|
|
953
1111
|
const { value, done } = await stream.iter.next();
|
|
954
1112
|
if (done)
|
|
@@ -1014,6 +1172,10 @@ pendingLines = []) {
|
|
|
1014
1172
|
err: err instanceof Error ? err.message : String(err),
|
|
1015
1173
|
});
|
|
1016
1174
|
}
|
|
1175
|
+
// Deliberately NOT short-circuited on `loggedOut`: breaking here
|
|
1176
|
+
// would end the pump for good and strand a later re-login. Fall
|
|
1177
|
+
// through instead — `reconnect()` no-ops while signed out, and the
|
|
1178
|
+
// next iteration parks on `logoutPark` at the top of the loop.
|
|
1017
1179
|
if (stdinClosed)
|
|
1018
1180
|
break;
|
|
1019
1181
|
// Stream ended. If a reconnect is ALREADY in progress (e.g. the
|
|
@@ -1039,11 +1201,25 @@ pendingLines = []) {
|
|
|
1039
1201
|
// session negotiates capabilities — but suppress that upstream
|
|
1040
1202
|
// reply, since the client already has this one.
|
|
1041
1203
|
if (msg.method === "initialize" && msg.id != null) {
|
|
1204
|
+
const clientInfo = rememberInitializeClientInfo(msg.params);
|
|
1205
|
+
if (clientInfo) {
|
|
1206
|
+
Object.assign(extraHeaders, clientInfoHeaders(clientInfo));
|
|
1207
|
+
log.info("bridge.agent_client", {
|
|
1208
|
+
clientName: clientInfo.name,
|
|
1209
|
+
clientVersion: clientInfo.version,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1042
1212
|
writeStdoutMessage({
|
|
1043
1213
|
jsonrpc: "2.0",
|
|
1044
1214
|
id: msg.id,
|
|
1045
1215
|
result: buildLocalInitializeResult(msg.params),
|
|
1046
1216
|
});
|
|
1217
|
+
// Signed out: the local reply is the whole answer. We will
|
|
1218
|
+
// not forward upstream, so do not arm a suppression that no
|
|
1219
|
+
// reply can ever consume — a leaked arm would swallow the
|
|
1220
|
+
// real reply if the client later reuses this id.
|
|
1221
|
+
if (loggedOut)
|
|
1222
|
+
return;
|
|
1047
1223
|
// Expect exactly one upstream reply to drop for this forward.
|
|
1048
1224
|
expectSuppressedReply(msg.id);
|
|
1049
1225
|
// Fall through: forward/buffer the initialize upstream too.
|
|
@@ -1092,6 +1268,12 @@ pendingLines = []) {
|
|
|
1092
1268
|
}
|
|
1093
1269
|
if (params.name === "memwal_logout") {
|
|
1094
1270
|
const result = handleLocalLogout();
|
|
1271
|
+
// Tear the session down before replying, so by the time
|
|
1272
|
+
// the client is told it is signed out that is actually
|
|
1273
|
+
// true. Only on success: if the credentials file could
|
|
1274
|
+
// not be removed the user is still signed in.
|
|
1275
|
+
if (!result.isError)
|
|
1276
|
+
invalidateSession();
|
|
1095
1277
|
writeStdoutMessage({
|
|
1096
1278
|
jsonrpc: "2.0",
|
|
1097
1279
|
id: msg.id,
|
|
@@ -1103,6 +1285,22 @@ pendingLines = []) {
|
|
|
1103
1285
|
return;
|
|
1104
1286
|
}
|
|
1105
1287
|
}
|
|
1288
|
+
// Signed out: refuse EVERY remaining request locally, not just
|
|
1289
|
+
// memory tool calls. `login`/`logout` returned above, and
|
|
1290
|
+
// `initialize`/`tools/list` are answered locally further up, so
|
|
1291
|
+
// anything still here would need the delegate key the user
|
|
1292
|
+
// deleted. Falling through instead would park it in
|
|
1293
|
+
// `pendingForward` — `sse` is null and `reconnect()` no-ops
|
|
1294
|
+
// while signed out — where it would either hang the client until
|
|
1295
|
+
// a login that may never come, or be flushed afterwards under a
|
|
1296
|
+
// NEW key the client never authorized it against. `failRequest`
|
|
1297
|
+
// picks the right shape per method: tool-result text for
|
|
1298
|
+
// `tools/call`, a JSON-RPC error for `ping` and friends, and
|
|
1299
|
+
// nothing at all for notifications.
|
|
1300
|
+
if (loggedOut) {
|
|
1301
|
+
failRequest(msg, "signed out", SIGNED_OUT_FAILURE);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1106
1304
|
// Fill in the configured default namespace for memory tool
|
|
1107
1305
|
// calls that didn't pass one. Mutates msg in place so the
|
|
1108
1306
|
// forwarded — and any replayed-on-reconnect — copy carries it.
|
|
@@ -1147,6 +1345,12 @@ pendingLines = []) {
|
|
|
1147
1345
|
if (activeCredentialGeneration !== credentialGeneration) {
|
|
1148
1346
|
await reconnect("post-credential-generation-mismatch", true);
|
|
1149
1347
|
}
|
|
1348
|
+
// A logout can land while the two awaits above are parked. The
|
|
1349
|
+
// key is gone by then, so answer locally instead of posting.
|
|
1350
|
+
if (loggedOut || !creds) {
|
|
1351
|
+
failRequest(msg, "signed out", SIGNED_OUT_FAILURE);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1150
1354
|
if (!sse) {
|
|
1151
1355
|
await reconnect("sse-missing");
|
|
1152
1356
|
return;
|
|
@@ -1185,7 +1389,9 @@ pendingLines = []) {
|
|
|
1185
1389
|
// into inFlight but not delivered) is closed out below.
|
|
1186
1390
|
break;
|
|
1187
1391
|
}
|
|
1188
|
-
|
|
1392
|
+
// `invalidateSession` nulls both; either one means this queue
|
|
1393
|
+
// must not be drained onto the relayer.
|
|
1394
|
+
if (!sse || !creds)
|
|
1189
1395
|
break; // lost the session; reconnect replays inFlight
|
|
1190
1396
|
const msg = pendingForward.shift();
|
|
1191
1397
|
try {
|
|
@@ -1258,6 +1464,9 @@ pendingLines = []) {
|
|
|
1258
1464
|
* Only `tools/call` shaped requests get the tool-result error envelope; any
|
|
1259
1465
|
* other id-bearing request gets a JSON-RPC error object (the correct shape
|
|
1260
1466
|
* for a non-tool request). */
|
|
1467
|
+
/** `opts` overrides the default "relayer unavailable" wording for callers
|
|
1468
|
+
* whose failure is not an outage — logout, for one, where blaming the
|
|
1469
|
+
* relayer would be actively misleading. */
|
|
1261
1470
|
function failRequest(msg, reason, opts = {}) {
|
|
1262
1471
|
if (msg.id == null)
|
|
1263
1472
|
return; // notification — nothing to answer
|
|
@@ -1359,7 +1568,7 @@ pendingLines = []) {
|
|
|
1359
1568
|
// finished after `credentialGeneration` moved — that was the double-flush.
|
|
1360
1569
|
const connectInBackground = (async () => {
|
|
1361
1570
|
let attempt = 0;
|
|
1362
|
-
while (!stdinClosed) {
|
|
1571
|
+
while (!stdinClosed && !loggedOut) {
|
|
1363
1572
|
if (reconnectPromise) {
|
|
1364
1573
|
await reconnectPromise;
|
|
1365
1574
|
continue;
|
|
@@ -1374,11 +1583,19 @@ pendingLines = []) {
|
|
|
1374
1583
|
}
|
|
1375
1584
|
const openingGeneration = credentialGeneration;
|
|
1376
1585
|
try {
|
|
1377
|
-
const candidate = await openSseStream(creds.relayerUrl, creds);
|
|
1586
|
+
const candidate = await openSseStream(creds.relayerUrl, creds, extraHeaders);
|
|
1378
1587
|
if (stdinClosed) {
|
|
1379
1588
|
candidate.abort();
|
|
1380
1589
|
break;
|
|
1381
1590
|
}
|
|
1591
|
+
// Signed out while this handshake was in flight. The loop guard
|
|
1592
|
+
// above only runs between iterations, so without this the
|
|
1593
|
+
// session would be published — an open, authenticated stream
|
|
1594
|
+
// holding the delegate key the user just deleted.
|
|
1595
|
+
if (loggedOut) {
|
|
1596
|
+
candidate.abort();
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1382
1599
|
if (openingGeneration !== credentialGeneration || sse) {
|
|
1383
1600
|
candidate.abort();
|
|
1384
1601
|
continue;
|