kojee-mcp 0.5.4 → 0.5.7
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 +24 -5
- package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
- package/dist/chunk-6SK6ITFE.js +142 -0
- package/dist/{chunk-62KH6VNQ.js → chunk-GATXJ6UT.js} +122 -190
- package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
- package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
- package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
- package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
- package/dist/cli.js +19 -24
- package/dist/control-token-4BUCTYQB.js +13 -0
- package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
- package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
- package/dist/ensure-join-7AEDJMPE.js +96 -0
- package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
- package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
- package/dist/index.d.ts +18 -15
- package/dist/index.js +7 -4
- package/dist/lib.d.ts +427 -0
- package/dist/lib.js +44 -0
- package/dist/parent-watchdog-RZLHYP7T.js +65 -0
- package/dist/reconnect-scheduler-ARV6JIWK.js +36 -0
- package/dist/resubscribe-G5OGDZJD.js +6 -0
- package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
- package/dist/{stop-hook-GO363SMD.js → stop-hook-46BJD55B.js} +15 -7
- package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
- package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
- package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
- package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
- package/dist/{wizard-Z5JA3YPV.js → wizard-UOXQYJLP.js} +7 -7
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -218,8 +218,8 @@ in it is Hermes-specific — and it is **OFF by default**.
|
|
|
218
218
|
|---|---|
|
|
219
219
|
| `KOJEE_WEBHOOK_URL` | Receiver endpoint (http/https). **Unset ⇒ sink OFF** (zero behavior change). |
|
|
220
220
|
| `KOJEE_WEBHOOK_SECRET` | HMAC-SHA256 key for the signature header. URL set but secret unset ⇒ sink **DISABLED with an error** (the proxy NEVER sends unsigned webhooks). |
|
|
221
|
-
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `
|
|
222
|
-
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a retryable failure —
|
|
221
|
+
| `KOJEE_WEBHOOK_TIMEOUT_MS` | Per-attempt request timeout (default `30000`). A timed-out attempt is **never retried** — see the retry policy below. |
|
|
222
|
+
| `KOJEE_WEBHOOK_MAX_RETRIES` | Retries on a **retryable** failure — connection errors (refused / reset / DNS) / 5xx / 408 / 429 (default `2`). Timeouts are **not** in this class. |
|
|
223
223
|
| `KOJEE_WEBHOOK_SIGNATURE_HEADER` | Header name carrying the signature (default `X-Kojee-Signature`). |
|
|
224
224
|
| `KOJEE_WEBHOOK_SIGNATURE_PREFIX` | Literal string prepended to the hex digest (default empty — bare hex). |
|
|
225
225
|
| `KOJEE_WEBHOOK_SIGNATURE_FORMAT` | Optional preset. `github` ⇒ header `X-Hub-Signature-256`, prefix `sha256=` (the GitHub-webhook convention). Explicit `_HEADER`/`_PREFIX` vars override the preset's corresponding value. Unknown values are **warned about once and ignored** — never fatal. |
|
|
@@ -275,10 +275,29 @@ constant beside it):
|
|
|
275
275
|
cursor on restart, so the same event may arrive more than once. There is **no
|
|
276
276
|
exactly-once** promise; the receiver's dedupe is what makes redelivery safe.
|
|
277
277
|
|
|
278
|
+
**Retry policy (0.5.6 — anti-storm).** A delivery attempt that **times out is
|
|
279
|
+
never retried**: the receiver may well have processed the event and just
|
|
280
|
+
answered slowly (the canonical receiver spawns an agent session *before*
|
|
281
|
+
responding), so a re-POST risks duplicate side effects by design. The sink logs
|
|
282
|
+
it as `delivered-unconfirmed (receiver slow)` and moves on. Retries happen
|
|
283
|
+
**only on genuine non-delivery**: connection errors (refused / reset / DNS —
|
|
284
|
+
the request never reached a receiver) and 5xx / 408 / 429 responses (the
|
|
285
|
+
receiver answered that it did *not* process the event), up to
|
|
286
|
+
`KOJEE_WEBHOOK_MAX_RETRIES` (default `2`). For those retried cases delivery is
|
|
287
|
+
still **at-least-once** and every redelivery carries the same
|
|
288
|
+
`X-Kojee-Delivery` id and identical body bytes — **dedupe by event id remains
|
|
289
|
+
the receiver's responsibility** (the `recipe.ts` contract). This fix removes
|
|
290
|
+
the timeout-driven storm, not the at-least-once semantics. If your receiver
|
|
291
|
+
does slow work, the robust pattern is still: verify the signature, dedupe,
|
|
292
|
+
**respond `202` immediately**, then process.
|
|
293
|
+
|
|
278
294
|
The sink is isolated and fire-and-forget: a slow, hanging, or failing webhook can
|
|
279
|
-
never delay or break the Monitor (event-log) or Channel wake paths
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
never delay or break the Monitor (event-log) or Channel wake paths (those run
|
|
296
|
+
before the webhook push). A slow delivery only delays *later webhook events*
|
|
297
|
+
behind it in the sink's own FIFO, and that backlog is bounded: the queue caps at
|
|
298
|
+
1000 (overflow logs + drops the newest; the resubscribe-replay redelivers after
|
|
299
|
+
a restart). The status log redacts the secret and strips any basic-auth
|
|
300
|
+
credentials embedded in `KOJEE_WEBHOOK_URL`.
|
|
282
301
|
|
|
283
302
|
## Wake Continuity (0.5.4)
|
|
284
303
|
|
|
@@ -14,10 +14,18 @@ import {
|
|
|
14
14
|
|
|
15
15
|
// src/auth/keystore.ts
|
|
16
16
|
import { importJWK, exportJWK, generateKeyPair } from "jose";
|
|
17
|
+
import crypto from "crypto";
|
|
17
18
|
import fs from "fs";
|
|
18
19
|
import os from "os";
|
|
19
20
|
import path from "path";
|
|
20
21
|
var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
|
|
22
|
+
function defaultPairedKeystorePath() {
|
|
23
|
+
return path.join(os.homedir(), ".kojee", "keypair.json");
|
|
24
|
+
}
|
|
25
|
+
function deriveKeystorePath(token) {
|
|
26
|
+
const hash = crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
27
|
+
return path.join(os.homedir(), ".kojee", `keypair-${hash}.json`);
|
|
28
|
+
}
|
|
21
29
|
async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
22
30
|
if (!fs.existsSync(keystorePath)) {
|
|
23
31
|
return null;
|
|
@@ -61,7 +69,7 @@ async function generateES256KeyPair() {
|
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
// src/gateway-client.ts
|
|
64
|
-
import
|
|
72
|
+
import crypto2 from "crypto";
|
|
65
73
|
var GatewayClient = class {
|
|
66
74
|
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
67
75
|
this.brokerUrl = brokerUrl;
|
|
@@ -97,7 +105,7 @@ var GatewayClient = class {
|
|
|
97
105
|
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
98
106
|
*/
|
|
99
107
|
static deriveSessionId(token) {
|
|
100
|
-
const hash =
|
|
108
|
+
const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
|
|
101
109
|
return hash.slice(0, 16);
|
|
102
110
|
}
|
|
103
111
|
/**
|
|
@@ -206,6 +214,8 @@ var GatewayClient = class {
|
|
|
206
214
|
};
|
|
207
215
|
|
|
208
216
|
export {
|
|
217
|
+
defaultPairedKeystorePath,
|
|
218
|
+
deriveKeystorePath,
|
|
209
219
|
loadKeystore,
|
|
210
220
|
saveKeystore,
|
|
211
221
|
generateES256KeyPair,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateES256KeyPair,
|
|
3
|
+
loadKeystore,
|
|
4
|
+
saveKeystore
|
|
5
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
6
|
+
|
|
7
|
+
// src/auth/auth-module.ts
|
|
8
|
+
import { calculateJwkThumbprint } from "jose";
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
|
|
11
|
+
// src/auth/registration.ts
|
|
12
|
+
async function registerKey(brokerUrl, token, publicJwk) {
|
|
13
|
+
const url = `${brokerUrl}/api/v1/bots/keys/register/`;
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
Authorization: `Bearer ${token}`
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({ public_jwk: publicJwk })
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Key registration failed (${response.status}): ${body}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return await response.json();
|
|
29
|
+
}
|
|
30
|
+
async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
|
|
31
|
+
const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: `Bearer ${token}`
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
bot_key_id: botKeyId,
|
|
40
|
+
challenge,
|
|
41
|
+
signature
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Key confirmation failed (${response.status}): ${body}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return await response.json();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/auth/auth-module.ts
|
|
54
|
+
async function signChallengeRaw(privateKey, data) {
|
|
55
|
+
const signer = crypto.createSign("SHA256");
|
|
56
|
+
signer.update(data);
|
|
57
|
+
signer.end();
|
|
58
|
+
const derSignature = signer.sign(
|
|
59
|
+
privateKey
|
|
60
|
+
);
|
|
61
|
+
return derSignature.toString("base64url");
|
|
62
|
+
}
|
|
63
|
+
var AuthModule = class {
|
|
64
|
+
constructor(token, brokerUrl, keystorePath) {
|
|
65
|
+
this.token = token;
|
|
66
|
+
this.brokerUrl = brokerUrl;
|
|
67
|
+
this.keystorePath = keystorePath;
|
|
68
|
+
}
|
|
69
|
+
token;
|
|
70
|
+
brokerUrl;
|
|
71
|
+
keystorePath;
|
|
72
|
+
privateKey = null;
|
|
73
|
+
publicJwk = null;
|
|
74
|
+
kid = null;
|
|
75
|
+
/**
|
|
76
|
+
* Ensure we have an enrolled keypair. Either loads from disk or
|
|
77
|
+
* performs the full enrollment flow.
|
|
78
|
+
*/
|
|
79
|
+
async ensureEnrolled() {
|
|
80
|
+
const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
|
|
81
|
+
if (existing) {
|
|
82
|
+
this.privateKey = existing.privateKey;
|
|
83
|
+
this.publicJwk = existing.publicJwk;
|
|
84
|
+
this.kid = existing.kid;
|
|
85
|
+
console.error("[auth] Loaded existing keypair from keystore");
|
|
86
|
+
return {
|
|
87
|
+
privateKey: existing.privateKey,
|
|
88
|
+
publicJwk: existing.publicJwk,
|
|
89
|
+
kid: existing.kid
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
console.error("[auth] No valid keystore found, enrolling new keypair...");
|
|
93
|
+
const { privateKey, publicJwk } = await generateES256KeyPair();
|
|
94
|
+
const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
|
|
95
|
+
console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
|
|
96
|
+
const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
97
|
+
const challengeData = `${regResult.challenge}.${thumbprint}`;
|
|
98
|
+
const signature = await signChallengeRaw(privateKey, challengeData);
|
|
99
|
+
const confirmResult = await confirmKey(
|
|
100
|
+
this.brokerUrl,
|
|
101
|
+
this.token,
|
|
102
|
+
regResult.bot_key_id,
|
|
103
|
+
regResult.challenge,
|
|
104
|
+
signature
|
|
105
|
+
);
|
|
106
|
+
if (!confirmResult.key_confirmed) {
|
|
107
|
+
throw new Error("Key enrollment failed: confirmation was rejected");
|
|
108
|
+
}
|
|
109
|
+
console.error("[auth] Key enrollment confirmed");
|
|
110
|
+
await saveKeystore(
|
|
111
|
+
privateKey,
|
|
112
|
+
publicJwk,
|
|
113
|
+
regResult.bot_key_id,
|
|
114
|
+
this.brokerUrl,
|
|
115
|
+
this.keystorePath
|
|
116
|
+
);
|
|
117
|
+
this.privateKey = privateKey;
|
|
118
|
+
this.publicJwk = publicJwk;
|
|
119
|
+
this.kid = regResult.bot_key_id;
|
|
120
|
+
return {
|
|
121
|
+
privateKey,
|
|
122
|
+
publicJwk,
|
|
123
|
+
kid: regResult.bot_key_id
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
getPrivateKey() {
|
|
127
|
+
if (!this.privateKey) throw new Error("Not enrolled yet");
|
|
128
|
+
return this.privateKey;
|
|
129
|
+
}
|
|
130
|
+
getPublicJwk() {
|
|
131
|
+
if (!this.publicJwk) throw new Error("Not enrolled yet");
|
|
132
|
+
return this.publicJwk;
|
|
133
|
+
}
|
|
134
|
+
getKid() {
|
|
135
|
+
if (!this.kid) throw new Error("Not enrolled yet");
|
|
136
|
+
return this.kid;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
AuthModule
|
|
142
|
+
};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
buildCatchUpNote,
|
|
3
|
+
buildMonitorSpawn,
|
|
4
|
+
buildReplyRecipe
|
|
5
|
+
} from "./chunk-X672ZN7V.js";
|
|
4
6
|
import {
|
|
5
7
|
deriveDiscoveryKey,
|
|
6
8
|
findClaudeAncestorPid
|
|
7
9
|
} from "./chunk-BJMASMKX.js";
|
|
8
10
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
buildReplyRecipe
|
|
12
|
-
} from "./chunk-X672ZN7V.js";
|
|
11
|
+
AuthModule
|
|
12
|
+
} from "./chunk-6SK6ITFE.js";
|
|
13
13
|
import {
|
|
14
|
-
GatewayClient
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "./chunk-
|
|
14
|
+
GatewayClient
|
|
15
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
16
|
+
import {
|
|
17
|
+
startEventStream
|
|
18
|
+
} from "./chunk-UEGQGXPY.js";
|
|
19
19
|
import {
|
|
20
20
|
translateToolCallResult
|
|
21
21
|
} from "./chunk-LDZXU3DW.js";
|
|
@@ -25,139 +25,6 @@ import fs2 from "fs";
|
|
|
25
25
|
import os from "os";
|
|
26
26
|
import path2 from "path";
|
|
27
27
|
|
|
28
|
-
// src/auth/auth-module.ts
|
|
29
|
-
import { calculateJwkThumbprint } from "jose";
|
|
30
|
-
import crypto from "crypto";
|
|
31
|
-
|
|
32
|
-
// src/auth/registration.ts
|
|
33
|
-
async function registerKey(brokerUrl, token, publicJwk) {
|
|
34
|
-
const url = `${brokerUrl}/api/v1/bots/keys/register/`;
|
|
35
|
-
const response = await fetch(url, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: {
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
Authorization: `Bearer ${token}`
|
|
40
|
-
},
|
|
41
|
-
body: JSON.stringify({ public_jwk: publicJwk })
|
|
42
|
-
});
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
const body = await response.text();
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Key registration failed (${response.status}): ${body}`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return await response.json();
|
|
50
|
-
}
|
|
51
|
-
async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
|
|
52
|
-
const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
|
|
53
|
-
const response = await fetch(url, {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: {
|
|
56
|
-
"Content-Type": "application/json",
|
|
57
|
-
Authorization: `Bearer ${token}`
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({
|
|
60
|
-
bot_key_id: botKeyId,
|
|
61
|
-
challenge,
|
|
62
|
-
signature
|
|
63
|
-
})
|
|
64
|
-
});
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
const body = await response.text();
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Key confirmation failed (${response.status}): ${body}`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return await response.json();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/auth/auth-module.ts
|
|
75
|
-
async function signChallengeRaw(privateKey, data) {
|
|
76
|
-
const signer = crypto.createSign("SHA256");
|
|
77
|
-
signer.update(data);
|
|
78
|
-
signer.end();
|
|
79
|
-
const derSignature = signer.sign(
|
|
80
|
-
privateKey
|
|
81
|
-
);
|
|
82
|
-
return derSignature.toString("base64url");
|
|
83
|
-
}
|
|
84
|
-
var AuthModule = class {
|
|
85
|
-
constructor(token, brokerUrl, keystorePath) {
|
|
86
|
-
this.token = token;
|
|
87
|
-
this.brokerUrl = brokerUrl;
|
|
88
|
-
this.keystorePath = keystorePath;
|
|
89
|
-
}
|
|
90
|
-
token;
|
|
91
|
-
brokerUrl;
|
|
92
|
-
keystorePath;
|
|
93
|
-
privateKey = null;
|
|
94
|
-
publicJwk = null;
|
|
95
|
-
kid = null;
|
|
96
|
-
/**
|
|
97
|
-
* Ensure we have an enrolled keypair. Either loads from disk or
|
|
98
|
-
* performs the full enrollment flow.
|
|
99
|
-
*/
|
|
100
|
-
async ensureEnrolled() {
|
|
101
|
-
const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
|
|
102
|
-
if (existing) {
|
|
103
|
-
this.privateKey = existing.privateKey;
|
|
104
|
-
this.publicJwk = existing.publicJwk;
|
|
105
|
-
this.kid = existing.kid;
|
|
106
|
-
console.error("[auth] Loaded existing keypair from keystore");
|
|
107
|
-
return {
|
|
108
|
-
privateKey: existing.privateKey,
|
|
109
|
-
publicJwk: existing.publicJwk,
|
|
110
|
-
kid: existing.kid
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
console.error("[auth] No valid keystore found, enrolling new keypair...");
|
|
114
|
-
const { privateKey, publicJwk } = await generateES256KeyPair();
|
|
115
|
-
const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
|
|
116
|
-
console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
|
|
117
|
-
const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
118
|
-
const challengeData = `${regResult.challenge}.${thumbprint}`;
|
|
119
|
-
const signature = await signChallengeRaw(privateKey, challengeData);
|
|
120
|
-
const confirmResult = await confirmKey(
|
|
121
|
-
this.brokerUrl,
|
|
122
|
-
this.token,
|
|
123
|
-
regResult.bot_key_id,
|
|
124
|
-
regResult.challenge,
|
|
125
|
-
signature
|
|
126
|
-
);
|
|
127
|
-
if (!confirmResult.key_confirmed) {
|
|
128
|
-
throw new Error("Key enrollment failed: confirmation was rejected");
|
|
129
|
-
}
|
|
130
|
-
console.error("[auth] Key enrollment confirmed");
|
|
131
|
-
await saveKeystore(
|
|
132
|
-
privateKey,
|
|
133
|
-
publicJwk,
|
|
134
|
-
regResult.bot_key_id,
|
|
135
|
-
this.brokerUrl,
|
|
136
|
-
this.keystorePath
|
|
137
|
-
);
|
|
138
|
-
this.privateKey = privateKey;
|
|
139
|
-
this.publicJwk = publicJwk;
|
|
140
|
-
this.kid = regResult.bot_key_id;
|
|
141
|
-
return {
|
|
142
|
-
privateKey,
|
|
143
|
-
publicJwk,
|
|
144
|
-
kid: regResult.bot_key_id
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
getPrivateKey() {
|
|
148
|
-
if (!this.privateKey) throw new Error("Not enrolled yet");
|
|
149
|
-
return this.privateKey;
|
|
150
|
-
}
|
|
151
|
-
getPublicJwk() {
|
|
152
|
-
if (!this.publicJwk) throw new Error("Not enrolled yet");
|
|
153
|
-
return this.publicJwk;
|
|
154
|
-
}
|
|
155
|
-
getKid() {
|
|
156
|
-
if (!this.kid) throw new Error("Not enrolled yet");
|
|
157
|
-
return this.kid;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
28
|
// src/tool-registry.ts
|
|
162
29
|
var ToolRegistry = class {
|
|
163
30
|
constructor(gateway) {
|
|
@@ -269,7 +136,19 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
|
269
136
|
const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
|
|
270
137
|
return intro + monitorSection + listenSection + advice;
|
|
271
138
|
}
|
|
272
|
-
function
|
|
139
|
+
async function executeToolCall(registry, name, args, hooks) {
|
|
140
|
+
const rawResult = await registry.callTool(name, args);
|
|
141
|
+
const result = translateToolCallResult(rawResult);
|
|
142
|
+
if (name === "tandem_join" && !result.isError) {
|
|
143
|
+
try {
|
|
144
|
+
hooks?.onTandemJoin?.();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
|
|
273
152
|
const capabilities = { tools: {} };
|
|
274
153
|
if (adapter.supportsChannels) {
|
|
275
154
|
capabilities.experimental = { "claude/channel": {} };
|
|
@@ -291,8 +170,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
|
|
|
291
170
|
});
|
|
292
171
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
293
172
|
const { name, arguments: args } = request.params;
|
|
294
|
-
const
|
|
295
|
-
const result = translateToolCallResult(rawResult);
|
|
173
|
+
const result = await executeToolCall(registry, name, args ?? {}, hooks);
|
|
296
174
|
return { content: result.content, isError: result.isError };
|
|
297
175
|
});
|
|
298
176
|
return server;
|
|
@@ -420,6 +298,7 @@ async function listTandemIds(gateway) {
|
|
|
420
298
|
return list.map((t) => {
|
|
421
299
|
if (typeof t === "string") return t;
|
|
422
300
|
const obj = t;
|
|
301
|
+
if (obj?.my_membership?.is_member !== true) return void 0;
|
|
423
302
|
return obj?.tandem_id ?? obj?.id;
|
|
424
303
|
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
425
304
|
} catch {
|
|
@@ -437,6 +316,50 @@ async function startProxy(config) {
|
|
|
437
316
|
console.error(
|
|
438
317
|
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
439
318
|
);
|
|
319
|
+
let activeStreamHandle = null;
|
|
320
|
+
const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-ARV6JIWK.js");
|
|
321
|
+
const joinReconnect = createJoinReconnectScheduler({
|
|
322
|
+
// BOOT-RACE (Bug B): report whether the stream handle was actually ready.
|
|
323
|
+
// `false` ⇒ activeStreamHandle is still null (tandem_join fired before the
|
|
324
|
+
// stream was set up) → the scheduler queues the reconnect and flushes it on
|
|
325
|
+
// notifyReady() once the handle is assigned (see below), instead of silently
|
|
326
|
+
// dropping it as the old `activeStreamHandle?.reconnect()` no-op did.
|
|
327
|
+
reconnect: () => {
|
|
328
|
+
if (!activeStreamHandle) return false;
|
|
329
|
+
activeStreamHandle.reconnect();
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
const onTandemJoin = () => joinReconnect.requestReconnect();
|
|
334
|
+
const teardownSteps = [];
|
|
335
|
+
let shuttingDown = false;
|
|
336
|
+
function shutdown(reason) {
|
|
337
|
+
if (shuttingDown) return;
|
|
338
|
+
shuttingDown = true;
|
|
339
|
+
activeStreamHandle?.();
|
|
340
|
+
for (const step of teardownSteps) {
|
|
341
|
+
try {
|
|
342
|
+
const maybe = step();
|
|
343
|
+
if (maybe && typeof maybe.catch === "function") {
|
|
344
|
+
maybe.catch((err) => {
|
|
345
|
+
console.error("[kojee-mcp] async shutdown step failed:", err?.message ?? err);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error("[kojee-mcp] shutdown step failed:", err?.message ?? err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
console.error(`[kojee-mcp] shutting down (${reason}), exiting`);
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
const ccPid = await findClaudeAncestorPid();
|
|
356
|
+
const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
|
|
357
|
+
await ensureJoinTandems({
|
|
358
|
+
gateway,
|
|
359
|
+
env: process.env["KOJEE_TANDEMS"],
|
|
360
|
+
listTandems: () => listTandemIds(gateway),
|
|
361
|
+
onJoined: () => joinReconnect.requestReconnect()
|
|
362
|
+
});
|
|
440
363
|
let tandemMembershipCount = -1;
|
|
441
364
|
try {
|
|
442
365
|
const bootIds = await listTandemIds(gateway);
|
|
@@ -448,19 +371,18 @@ async function startProxy(config) {
|
|
|
448
371
|
let server;
|
|
449
372
|
if (adapter.supportsChannels) {
|
|
450
373
|
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
451
|
-
const { startHookServer } = await import("./hook-server-
|
|
374
|
+
const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
|
|
452
375
|
const {
|
|
453
376
|
writeDiscoveryByKey,
|
|
454
377
|
cleanupDiscoveryByKey,
|
|
455
378
|
sweepStaleDiscovery
|
|
456
379
|
} = await import("./session-discovery-FNMJGFPM.js");
|
|
457
380
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
458
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
459
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
460
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
381
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
382
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
383
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
461
384
|
sweepStaleDiscovery();
|
|
462
385
|
sweepStaleEventLogs();
|
|
463
|
-
const ccPid = await findClaudeAncestorPid();
|
|
464
386
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
465
387
|
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
466
388
|
const eventLog = startEventLog({ key: discoveryKey });
|
|
@@ -487,14 +409,16 @@ async function startProxy(config) {
|
|
|
487
409
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
488
410
|
});
|
|
489
411
|
}
|
|
490
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path
|
|
491
|
-
|
|
412
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
|
|
413
|
+
onTandemJoin
|
|
414
|
+
});
|
|
415
|
+
const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
|
|
492
416
|
let controlToken = null;
|
|
493
417
|
try {
|
|
494
418
|
controlToken = issueControlToken();
|
|
495
419
|
} catch (err) {
|
|
496
420
|
console.error(
|
|
497
|
-
"[kojee-mcp] control token write failed \u2014 POST /send disabled:",
|
|
421
|
+
"[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
|
|
498
422
|
err.message
|
|
499
423
|
);
|
|
500
424
|
}
|
|
@@ -504,7 +428,10 @@ async function startProxy(config) {
|
|
|
504
428
|
port: 0,
|
|
505
429
|
queue,
|
|
506
430
|
adapter,
|
|
507
|
-
|
|
431
|
+
// 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
|
|
432
|
+
// reads (GET /poll, GET /status). When token issuance failed both stay
|
|
433
|
+
// available-but-degraded: /send answers 503, the reads stay open.
|
|
434
|
+
...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
|
|
508
435
|
getStreamState: () => streamHandle ? streamHandle.getState() : {
|
|
509
436
|
connected: false,
|
|
510
437
|
connectedSince: null,
|
|
@@ -539,16 +466,16 @@ async function startProxy(config) {
|
|
|
539
466
|
authMode: config.authMode ?? "paired"
|
|
540
467
|
});
|
|
541
468
|
const cleanupDiscoveryFile = () => cleanupDiscoveryByKey(discoveryKey);
|
|
542
|
-
process.on("exit", () =>
|
|
543
|
-
process.on("SIGINT", () => {
|
|
469
|
+
process.on("exit", () => {
|
|
544
470
|
cleanupDiscoveryFile();
|
|
545
|
-
|
|
471
|
+
eventLog.cleanup();
|
|
546
472
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
process.exit(0);
|
|
473
|
+
teardownSteps.push(() => {
|
|
474
|
+
void webhookSink?.stop();
|
|
550
475
|
});
|
|
551
|
-
|
|
476
|
+
teardownSteps.push(() => cleanupDiscoveryFile());
|
|
477
|
+
teardownSteps.push(() => eventLog.cleanup());
|
|
478
|
+
teardownSteps.push(() => hookServer.stop());
|
|
552
479
|
streamHandle = await startEventStream({
|
|
553
480
|
brokerUrl: config.url,
|
|
554
481
|
token: config.token,
|
|
@@ -584,24 +511,14 @@ async function startProxy(config) {
|
|
|
584
511
|
};
|
|
585
512
|
})()
|
|
586
513
|
});
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
cancelStream();
|
|
590
|
-
void webhookSink?.stop();
|
|
591
|
-
cleanupDiscoveryFile();
|
|
592
|
-
eventLog.cleanup();
|
|
593
|
-
hookServer.stop().finally(() => {
|
|
594
|
-
console.error("[kojee-mcp] stdin closed, exiting");
|
|
595
|
-
process.exit(0);
|
|
596
|
-
});
|
|
597
|
-
});
|
|
514
|
+
activeStreamHandle = streamHandle;
|
|
515
|
+
joinReconnect.notifyReady();
|
|
598
516
|
} else if (needsWebhookEventStream()) {
|
|
599
517
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
600
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
601
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
602
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
518
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
519
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
520
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
603
521
|
sweepStaleEventLogs();
|
|
604
|
-
const ccPid = await findClaudeAncestorPid();
|
|
605
522
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
606
523
|
const discoveryKey = deriveDiscoveryKey(projectDir, ccPid);
|
|
607
524
|
const eventLog = startEventLog({ key: discoveryKey });
|
|
@@ -627,8 +544,14 @@ async function startProxy(config) {
|
|
|
627
544
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
628
545
|
});
|
|
629
546
|
}
|
|
630
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount
|
|
547
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
|
|
548
|
+
onTandemJoin
|
|
549
|
+
});
|
|
631
550
|
process.on("exit", () => eventLog.cleanup());
|
|
551
|
+
teardownSteps.push(() => {
|
|
552
|
+
void webhookSink?.stop();
|
|
553
|
+
});
|
|
554
|
+
teardownSteps.push(() => eventLog.cleanup());
|
|
632
555
|
const streamHandle = await startEventStream({
|
|
633
556
|
brokerUrl: config.url,
|
|
634
557
|
token: config.token,
|
|
@@ -649,19 +572,28 @@ async function startProxy(config) {
|
|
|
649
572
|
};
|
|
650
573
|
})()
|
|
651
574
|
});
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
void webhookSink?.stop();
|
|
655
|
-
eventLog.cleanup();
|
|
656
|
-
console.error("[kojee-mcp] stdin closed, exiting");
|
|
657
|
-
process.exit(0);
|
|
658
|
-
});
|
|
575
|
+
activeStreamHandle = streamHandle;
|
|
576
|
+
joinReconnect.notifyReady();
|
|
659
577
|
} else {
|
|
660
578
|
server = createMcpServer(registry, adapter, tandemMembershipCount);
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
579
|
+
}
|
|
580
|
+
process.stdin.on("end", () => shutdown("stdin end"));
|
|
581
|
+
process.stdin.on("close", () => shutdown("stdin close"));
|
|
582
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
583
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
584
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
585
|
+
if (ccPid !== null) {
|
|
586
|
+
const { createParentWatchdog } = await import("./parent-watchdog-RZLHYP7T.js");
|
|
587
|
+
const watchdog = createParentWatchdog({
|
|
588
|
+
ccPid,
|
|
589
|
+
onParentGone: () => shutdown("parent (Claude Code) gone")
|
|
664
590
|
});
|
|
591
|
+
watchdog.start();
|
|
592
|
+
teardownSteps.push(() => watchdog.stop());
|
|
593
|
+
} else {
|
|
594
|
+
console.error(
|
|
595
|
+
"[kojee-mcp] no Claude Code ancestor found \u2014 parent-liveness watchdog NOT armed (stdin/signal handlers still cover clean exits)"
|
|
596
|
+
);
|
|
665
597
|
}
|
|
666
598
|
await startMcpServer(server);
|
|
667
599
|
}
|
|
@@ -698,7 +630,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
698
630
|
}
|
|
699
631
|
|
|
700
632
|
export {
|
|
701
|
-
AuthModule,
|
|
702
633
|
VERSION,
|
|
634
|
+
listTandemIds,
|
|
703
635
|
startProxy
|
|
704
636
|
};
|