@code-yeongyu/senpi 2026.10.25-1 → 2026.10.25-3
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 +36 -0
- package/dist/bundle/chunks/{args-JXULI4EH.js → args-B444OQBK.js} +1 -1
- package/dist/bundle/chunks/{auth-4NUNUSTF.js → auth-C77UQA55.js} +1 -1
- package/dist/bundle/chunks/{chunk-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
- package/dist/bundle/chunks/chunk-6O4SEW2W.js +5 -0
- package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
- package/dist/bundle/chunks/{chunk-YDCRA5ES.js → chunk-7W7OJDIY.js} +4 -4
- package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
- package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-C5UFJ7B3.js} +1 -1
- package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-CNL6QSUB.js} +4 -4
- package/dist/bundle/chunks/{chunk-BTNYLNWZ.js → chunk-FFQOEC76.js} +5 -5
- package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
- package/dist/bundle/chunks/chunk-H3HNMY2W.js +13 -0
- package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-HVJI5T75.js} +1 -1
- package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-JA2R2ZSE.js} +1 -1
- package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
- package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
- package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
- package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
- package/dist/bundle/chunks/{chunk-EYBDBR7V.js → chunk-MJ5NNWX4.js} +1 -1
- package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-O3Q2ET6W.js} +2 -5
- package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
- package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-ODTGC6YU.js} +1 -1
- package/dist/bundle/chunks/chunk-P3M2PF6K.js +27 -0
- package/dist/bundle/chunks/chunk-SWJXRC2I.js +5 -0
- package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
- package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
- package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-UQNRW3VU.js} +1 -1
- package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
- package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
- package/dist/bundle/chunks/{chunk-7WAJCESX.js → chunk-WLHNEUGD.js} +1 -1
- package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
- package/dist/bundle/chunks/{chunk-URBJS24R.js → chunk-YRF4I43T.js} +35 -35
- package/dist/bundle/chunks/chunk-Z3UYNSVR.js +3 -0
- package/dist/bundle/chunks/cli-main-HEKFFB5S.js +3 -0
- package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
- package/dist/bundle/chunks/dispatch-NYAULNVW.js +9 -0
- package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
- package/dist/bundle/chunks/host-command-Z3WPHJOW.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-ONQIMWMX.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +9 -7
- package/dist/bundle/chunks/host-session-command-J65ENSV5.js +20 -0
- package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-3CWE4265.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +2 -0
- package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-M7HVUMRH.js → multi-session-host-YI2WZOET.js} +12 -9
- package/dist/bundle/chunks/{package-manager-cli-FSR5P6YL.js → package-manager-cli-GHZPJMRF.js} +1 -1
- package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-U7YQMC7H.js → rotation-stream-ZPVCFOVD.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-FFEQS744.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-YWG6D6ZF.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +3 -0
- package/dist/bundle/chunks/{session-picker-KQUFWDQC.js → session-picker-25H5KNYP.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +285 -271
- package/dist/bundle/chunks/{stdio-IEC635SY.js → stdio-YXDKKKQZ.js} +1 -1
- package/dist/bundle/chunks/{streamableHttp-KJI5KE4K.js → streamableHttp-DKIBUWVY.js} +1 -1
- package/dist/bundle/chunks/{types-LLNV23LW.js → types-BJUEQ4II.js} +1 -1
- package/dist/bundle/chunks/zod-compat-RJDXCU7S.js +2 -0
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +9 -7
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +9 -0
- package/dist/cli/host-command-args.d.ts +1 -0
- package/dist/cli/host-command-args.js +7 -1
- package/dist/cli/host-command.js +3 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.js +50 -16
- package/dist/core/engine-turn-limit.d.ts +2 -0
- package/dist/core/engine-turn-limit.js +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
- package/dist/core/extensions/builtin/goal/index.js +1 -0
- package/dist/core/extensions/builtin/goal/lifecycle-facts.d.ts +3 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
- package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
- package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/connection.js +46 -1
- package/dist/core/extensions/builtin/mcp/elicitation.d.ts +6 -1
- package/dist/core/extensions/builtin/mcp/elicitation.js +56 -2
- package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
- package/dist/core/extensions/builtin/mcp/health.js +5 -0
- package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
- package/dist/core/extensions/builtin/mcp/index.js +2 -0
- package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
- package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
- package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
- package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
- package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
- package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
- package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
- package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
- package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
- package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
- package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
- package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
- package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
- package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
- package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
- package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
- package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
- package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
- package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
- package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
- package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
- package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/resources.js +19 -4
- package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
- package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/service.js +79 -4
- package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
- package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
- package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
- package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
- package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
- package/dist/core/extensions/runner.js +3 -2
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/lifecycle-ledger.d.ts +1 -0
- package/dist/core/lifecycle-ledger.js +7 -2
- package/dist/core/lifecycle-record.js +3 -0
- package/dist/core/lifecycle-runtime-facts.js +3 -0
- package/dist/core/lifecycle-session-binding.d.ts +11 -1
- package/dist/core/lifecycle-session-binding.js +35 -4
- package/dist/core/lifecycle-stop.d.ts +41 -0
- package/dist/core/lifecycle-stop.js +92 -0
- package/dist/core/mcp-form-types.d.ts +89 -0
- package/dist/core/mcp-form-types.js +23 -0
- package/dist/core/mcp-mrtr-presentations.d.ts +15 -0
- package/dist/core/mcp-mrtr-presentations.js +17 -0
- package/dist/core/run-resume-turn.d.ts +1 -0
- package/dist/core/run-resume-turn.js +4 -3
- package/dist/core/session-lifecycle-types.d.ts +15 -0
- package/dist/core/session-lifecycle.d.ts +17 -1
- package/dist/core/session-lifecycle.js +40 -2
- package/dist/core/stop-resume-directive.d.ts +1 -1
- package/dist/core/stop-resume-directive.js +10 -1
- package/dist/main.js +5 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
- package/dist/modes/interactive/components/extension-selector.js +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.js +35 -8
- package/dist/modes/interactive/mcp-local-presenter.d.ts +5 -0
- package/dist/modes/interactive/mcp-local-presenter.js +109 -0
- package/dist/modes/interactive/pause-footer.d.ts +5 -0
- package/dist/modes/interactive/pause-footer.js +2 -0
- package/dist/modes/rpc/connection-handler.d.ts +1 -0
- package/dist/modes/rpc/connection-handler.js +7 -1
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-ensure-start.js +13 -2
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +1 -0
- package/dist/modes/rpc/host-handoff.d.ts +2 -0
- package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
- package/dist/modes/rpc/host-handover-wire.js +1 -0
- package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
- package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
- package/dist/modes/rpc/host-reservations.js +3 -2
- package/dist/modes/rpc/host-runner.d.ts +2 -0
- package/dist/modes/rpc/host-runner.js +8 -6
- package/dist/modes/rpc/host-stalled-evidence.js +2 -1
- package/dist/modes/rpc/host-successor.js +11 -1
- package/dist/modes/rpc/mcp-form-dialog.d.ts +45 -0
- package/dist/modes/rpc/mcp-form-dialog.js +159 -0
- package/dist/modes/rpc/mcp-form-worker.d.ts +14 -0
- package/dist/modes/rpc/mcp-form-worker.js +167 -0
- package/dist/modes/rpc/mcp-presenter-authority.d.ts +53 -0
- package/dist/modes/rpc/mcp-presenter-authority.js +309 -0
- package/dist/modes/rpc/mcp-presenter-bootstrap.d.ts +8 -0
- package/dist/modes/rpc/mcp-presenter-bootstrap.js +111 -0
- package/dist/modes/rpc/mcp-presenter-control.d.ts +24 -0
- package/dist/modes/rpc/mcp-presenter-control.js +330 -0
- package/dist/modes/rpc/multi-session-host.d.ts +3 -0
- package/dist/modes/rpc/multi-session-host.js +36 -1
- package/dist/modes/rpc/session-binding.js +6 -1
- package/dist/modes/rpc/session-event-writer.d.ts +14 -0
- package/dist/modes/rpc/session-event-writer.js +128 -1
- package/dist/modes/rpc/session-router-routing.js +1 -0
- package/dist/modes/rpc/session-worker-client.d.ts +3 -0
- package/dist/modes/rpc/session-worker-client.js +89 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +41 -0
- package/dist/modes/rpc/session-worker.js +9 -0
- package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
- package/dist/utils/process-identity.d.ts +7 -0
- package/dist/utils/process-identity.js +32 -5
- package/docs/extensions.md +5 -0
- package/docs/mcp.md +20 -3
- package/docs/rpc.md +111 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-3VOG6WFU.js +0 -27
- package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
- package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
- package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
- package/dist/bundle/chunks/chunk-II22E3CV.js +0 -3
- package/dist/bundle/chunks/chunk-J5P2VL3Y.js +0 -5
- package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
- package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
- package/dist/bundle/chunks/cli-main-TABBP55A.js +0 -3
- package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
- package/dist/bundle/chunks/host-command-OQR4RDTM.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +0 -3
- package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +0 -20
- package/dist/bundle/chunks/interactive-mode-DGQNSXUA.js +0 -2
- package/dist/bundle/chunks/rpc-mode-SZAJNKKL.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-WEF3XTJR.js +0 -3
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createPublicKey, randomBytes, randomUUID, verify } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { Duplex, PassThrough, Readable, Writable } from "node:stream";
|
|
4
|
+
import { isRecord } from "../../core/extensions/builtin/mcp/modern/form-schema.js";
|
|
5
|
+
import { attachJsonlLineReader } from "./jsonl.js";
|
|
6
|
+
/** Separate authenticated control endpoint. Ordinary RPC credentials cannot enroll or acquire. */
|
|
7
|
+
export class McpPresenterControl {
|
|
8
|
+
constructor(writer, path) {
|
|
9
|
+
this.principals = new Map();
|
|
10
|
+
this.controls = new Set();
|
|
11
|
+
this.revokeControls = new Map();
|
|
12
|
+
this.backendGenerations = new Map();
|
|
13
|
+
this.successors = new Map();
|
|
14
|
+
this.writer = writer;
|
|
15
|
+
this.authority = writer.enableMcpPresenters();
|
|
16
|
+
this.path = path;
|
|
17
|
+
this.server = createServer((stream) => this.manage(stream));
|
|
18
|
+
}
|
|
19
|
+
listen() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.server.once("error", reject);
|
|
22
|
+
this.server.listen(this.path, () => {
|
|
23
|
+
this.server.off("error", reject);
|
|
24
|
+
resolve();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** A deferred handover is spawned by the old host, not the CLI that already returned. */
|
|
29
|
+
successorBootstrap() {
|
|
30
|
+
const owner = this.launchOwner;
|
|
31
|
+
if (!owner || owner.stream.destroyed)
|
|
32
|
+
return undefined;
|
|
33
|
+
const id = randomUUID();
|
|
34
|
+
const toChild = new PassThrough();
|
|
35
|
+
const fromChild = new PassThrough();
|
|
36
|
+
const endpoint = Duplex.from({ readable: Readable.toWeb(toChild), writable: Writable.toWeb(fromChild) });
|
|
37
|
+
this.successors.set(id, { stream: owner.stream, write: toChild });
|
|
38
|
+
const stop = privateFrames(fromChild, (value) => {
|
|
39
|
+
if (!isRecord(value)) {
|
|
40
|
+
endpoint.destroy();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (value.type === "challenge") {
|
|
44
|
+
send(owner.stream, { type: "mcp_presenter_successor_challenge", id, challenge: value });
|
|
45
|
+
}
|
|
46
|
+
else if (value.type === "enrolled") {
|
|
47
|
+
send(owner.stream, { type: "mcp_presenter_successor_enrolled", id, enrollment: value });
|
|
48
|
+
stop();
|
|
49
|
+
this.successors.delete(id);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
endpoint.destroy();
|
|
53
|
+
});
|
|
54
|
+
endpoint.once("close", () => {
|
|
55
|
+
stop();
|
|
56
|
+
this.successors.delete(id);
|
|
57
|
+
toChild.destroy();
|
|
58
|
+
fromChild.destroy();
|
|
59
|
+
});
|
|
60
|
+
toChild.write(`${JSON.stringify({
|
|
61
|
+
type: "enroll",
|
|
62
|
+
publicKey: owner.principal.key.export({ type: "spki", format: "pem" }),
|
|
63
|
+
backendGeneration: owner.backendGeneration,
|
|
64
|
+
})}\n`);
|
|
65
|
+
return endpoint;
|
|
66
|
+
}
|
|
67
|
+
/** Only the inherited launch channel may introduce the installation public key. */
|
|
68
|
+
bootstrap(stream) {
|
|
69
|
+
let key;
|
|
70
|
+
let backend;
|
|
71
|
+
let transcript;
|
|
72
|
+
const id = randomUUID();
|
|
73
|
+
const stop = privateFrames(stream, (value) => {
|
|
74
|
+
if (!key) {
|
|
75
|
+
if (!isRecord(value) ||
|
|
76
|
+
value.type !== "enroll" ||
|
|
77
|
+
typeof value.publicKey !== "string" ||
|
|
78
|
+
typeof value.backendGeneration !== "string") {
|
|
79
|
+
stream.destroy();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
key = createPublicKey(value.publicKey);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
stream.destroy();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
90
|
+
stream.destroy();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
backend = value.backendGeneration;
|
|
94
|
+
transcript = JSON.stringify([
|
|
95
|
+
"mcp-presenter-enroll-v1",
|
|
96
|
+
this.authority.instance,
|
|
97
|
+
backend,
|
|
98
|
+
randomBytes(32).toString("hex"),
|
|
99
|
+
]);
|
|
100
|
+
send(stream, { type: "challenge", transcript, hostInstance: this.authority.instance });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!transcript || !signed(value, transcript, key)) {
|
|
104
|
+
stream.destroy();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.principals.set(id, { id, key, sessions: new WeakSet() });
|
|
108
|
+
send(stream, {
|
|
109
|
+
type: "enrolled",
|
|
110
|
+
principalId: id,
|
|
111
|
+
hostInstance: this.authority.instance,
|
|
112
|
+
backendGeneration: backend,
|
|
113
|
+
endpoint: this.path,
|
|
114
|
+
});
|
|
115
|
+
stop();
|
|
116
|
+
stream.end();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
dispose() {
|
|
120
|
+
this.authority.dispose();
|
|
121
|
+
for (const stream of this.controls)
|
|
122
|
+
stream.destroy();
|
|
123
|
+
this.controls.clear();
|
|
124
|
+
for (const successor of this.successors.values())
|
|
125
|
+
successor.write.destroy();
|
|
126
|
+
this.successors.clear();
|
|
127
|
+
this.principals.clear();
|
|
128
|
+
this.server.close();
|
|
129
|
+
}
|
|
130
|
+
manage(stream) {
|
|
131
|
+
if (this.controls.size >= 64) {
|
|
132
|
+
stream.destroy();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.controls.add(stream);
|
|
136
|
+
let principal;
|
|
137
|
+
let owner;
|
|
138
|
+
let transcript;
|
|
139
|
+
let pending;
|
|
140
|
+
let backendGeneration;
|
|
141
|
+
let deadline = performance.now() + 30_000;
|
|
142
|
+
let challengeTimer = setTimeout(() => stream.destroy(), 30_000);
|
|
143
|
+
const registrations = new Map();
|
|
144
|
+
const revoke = () => {
|
|
145
|
+
clearTimeout(challengeTimer);
|
|
146
|
+
for (const registration of registrations.values())
|
|
147
|
+
owner?.revoke(registration);
|
|
148
|
+
registrations.clear();
|
|
149
|
+
this.controls.delete(stream);
|
|
150
|
+
this.revokeControls.delete(stream);
|
|
151
|
+
this.backendGenerations.delete(stream);
|
|
152
|
+
if (this.launchOwner?.stream === stream)
|
|
153
|
+
this.launchOwner = undefined;
|
|
154
|
+
for (const [id, successor] of this.successors)
|
|
155
|
+
if (successor.stream === stream) {
|
|
156
|
+
successor.write.destroy();
|
|
157
|
+
this.successors.delete(id);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
this.revokeControls.set(stream, revoke);
|
|
161
|
+
stream.once("close", revoke);
|
|
162
|
+
const stop = privateFrames(stream, (value) => {
|
|
163
|
+
if (!isRecord(value)) {
|
|
164
|
+
stream.destroy();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (value.type === "mcp_presenter_successor_proof" && typeof value.id === "string") {
|
|
168
|
+
const successor = this.successors.get(value.id);
|
|
169
|
+
if (successor?.stream !== stream || typeof value.signature !== "string") {
|
|
170
|
+
stream.destroy();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
successor.write.write(`${JSON.stringify({ type: "proof", signature: value.signature })}\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (value.type === "challenge") {
|
|
177
|
+
if (pending ||
|
|
178
|
+
typeof value.principalId !== "string" ||
|
|
179
|
+
typeof value.backendGeneration !== "string" ||
|
|
180
|
+
!isRecord(value.command)) {
|
|
181
|
+
stream.destroy();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const requested = this.principals.get(value.principalId);
|
|
185
|
+
if (!requested || (principal && principal !== requested)) {
|
|
186
|
+
stream.destroy();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
principal = requested;
|
|
190
|
+
backendGeneration = value.backendGeneration;
|
|
191
|
+
pending = value.command;
|
|
192
|
+
deadline = performance.now() + 30_000;
|
|
193
|
+
clearTimeout(challengeTimer);
|
|
194
|
+
challengeTimer = setTimeout(() => stream.destroy(), 30_000);
|
|
195
|
+
transcript = JSON.stringify([
|
|
196
|
+
"mcp-presenter-control-v1",
|
|
197
|
+
this.authority.instance,
|
|
198
|
+
value.backendGeneration,
|
|
199
|
+
randomUUID(),
|
|
200
|
+
randomBytes(32).toString("hex"),
|
|
201
|
+
pending,
|
|
202
|
+
]);
|
|
203
|
+
send(stream, { type: "challenge", transcript, hostInstance: this.authority.instance });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!principal ||
|
|
207
|
+
!pending ||
|
|
208
|
+
!transcript ||
|
|
209
|
+
performance.now() >= deadline ||
|
|
210
|
+
!signed(value, transcript, principal.key)) {
|
|
211
|
+
stream.destroy();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const command = pending;
|
|
215
|
+
clearTimeout(challengeTimer);
|
|
216
|
+
pending = undefined;
|
|
217
|
+
transcript = undefined;
|
|
218
|
+
owner ??= this.authority.enroll(principal.id);
|
|
219
|
+
if (backendGeneration === undefined) {
|
|
220
|
+
stream.destroy();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const [oldStream, old] of this.backendGenerations) {
|
|
224
|
+
if (old.principal === principal && old.generation !== backendGeneration) {
|
|
225
|
+
this.revokeControls.get(oldStream)?.();
|
|
226
|
+
oldStream.destroy();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.backendGenerations.set(stream, { principal, generation: backendGeneration });
|
|
230
|
+
this.launchOwner = { stream, principal, backendGeneration };
|
|
231
|
+
const id = typeof command.id === "string" ? command.id : undefined;
|
|
232
|
+
try {
|
|
233
|
+
switch (command.type) {
|
|
234
|
+
case "mcp_presenter_assign": {
|
|
235
|
+
const binding = typeof command.sessionId === "string"
|
|
236
|
+
? this.writer.getMcpFormBinding(command.sessionId)
|
|
237
|
+
: undefined;
|
|
238
|
+
if (!binding || binding.epoch !== command.bindingEpoch || command.purpose !== "mcp_form")
|
|
239
|
+
throw new Error("presenter_not_authorized");
|
|
240
|
+
// Only a signed native-supervisor decision assigns an opened/adopted session.
|
|
241
|
+
// Enrollment and ordinary RPC attachment do not imply assignment.
|
|
242
|
+
principal.sessions.add(binding);
|
|
243
|
+
send(stream, { type: "mcp_presenter_assigned", id, sessionId: command.sessionId });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
case "mcp_presenter_acquire": {
|
|
247
|
+
if (command.hostInstance !== this.authority.instance ||
|
|
248
|
+
typeof command.sessionId !== "string" ||
|
|
249
|
+
typeof command.bindingEpoch !== "string" ||
|
|
250
|
+
typeof command.connectionNonce !== "string" ||
|
|
251
|
+
(command.kind !== "desktop" && command.kind !== "gateway"))
|
|
252
|
+
throw new Error("presenter_not_authorized");
|
|
253
|
+
const binding = this.writer.getMcpFormBinding(command.sessionId);
|
|
254
|
+
const connection = this.writer.findMcpPresenterConnection(command.connectionNonce);
|
|
255
|
+
if (!binding ||
|
|
256
|
+
!principal.sessions.has(binding) ||
|
|
257
|
+
binding.epoch !== command.bindingEpoch ||
|
|
258
|
+
!connection)
|
|
259
|
+
throw new Error("presenter_not_authorized");
|
|
260
|
+
const capabilities = this.writer.getConnectionCapabilities(connection.id);
|
|
261
|
+
if (!capabilities?.includes("interactive_user") || !capabilities.includes("mcp_form_presenter_v1"))
|
|
262
|
+
throw new Error("presenter_not_authorized");
|
|
263
|
+
const context = this.authority.connectionContext(connection, command.sessionId);
|
|
264
|
+
if (!context ||
|
|
265
|
+
command.purpose !== "mcp_form" ||
|
|
266
|
+
command.attachmentId !== context.attachmentId ||
|
|
267
|
+
command.attachmentEpoch !== context.attachmentEpoch)
|
|
268
|
+
throw new Error("presenter_not_authorized");
|
|
269
|
+
const registration = owner.register(binding, connection, { kind: command.kind });
|
|
270
|
+
const grant = owner.acquire(registration);
|
|
271
|
+
registrations.set(registration.id, registration);
|
|
272
|
+
send(stream, { type: "mcp_presenter_acquired", id, registrationId: registration.id, ...grant });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
case "mcp_presenter_revoke": {
|
|
276
|
+
const registration = typeof command.registrationId === "string"
|
|
277
|
+
? registrations.get(command.registrationId)
|
|
278
|
+
: undefined;
|
|
279
|
+
if (!registration)
|
|
280
|
+
throw new Error("presenter_not_authorized");
|
|
281
|
+
owner.revoke(registration);
|
|
282
|
+
registrations.delete(registration.id);
|
|
283
|
+
send(stream, { type: "mcp_presenter_revoked", id });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
throw new Error("presenter_not_authorized");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
send(stream, { type: "mcp_presenter_error", id, error: "presenter_not_authorized" });
|
|
292
|
+
}
|
|
293
|
+
}, false);
|
|
294
|
+
stream.once("close", stop);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function signed(value, transcript, key) {
|
|
298
|
+
if (!isRecord(value) ||
|
|
299
|
+
value.type !== "proof" ||
|
|
300
|
+
typeof value.signature !== "string" ||
|
|
301
|
+
!/^[a-f0-9]{128}$/.test(value.signature))
|
|
302
|
+
return false;
|
|
303
|
+
return verify(null, Buffer.from(transcript), key, Buffer.from(value.signature, "hex"));
|
|
304
|
+
}
|
|
305
|
+
function send(stream, value) {
|
|
306
|
+
if (!stream.write(`${JSON.stringify(value)}\n`))
|
|
307
|
+
stream.destroy();
|
|
308
|
+
}
|
|
309
|
+
/** Bound before parsing. Neither malformed bodies nor parser exceptions enter diagnostics. */
|
|
310
|
+
function privateFrames(stream, receive, deadline = true) {
|
|
311
|
+
const timer = deadline ? setTimeout(() => stream.destroy(), 30_000) : undefined;
|
|
312
|
+
const detach = attachJsonlLineReader(stream, (line) => {
|
|
313
|
+
try {
|
|
314
|
+
receive(JSON.parse(line));
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
stream.destroy();
|
|
318
|
+
}
|
|
319
|
+
}, { maxLineLength: 16 * 1024, onOversizedLine: () => stream.destroy() });
|
|
320
|
+
stream.on("error", () => stream.destroy());
|
|
321
|
+
stream.once("close", () => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
detach();
|
|
324
|
+
});
|
|
325
|
+
return () => {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
detach();
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=mcp-presenter-control.js.map
|
|
@@ -7,6 +7,9 @@ import { type HostIdleOverrides } from "./host-idle-policy.ts";
|
|
|
7
7
|
import { type RpcBindingFactory, SessionCommandRouter } from "./session-command-router.ts";
|
|
8
8
|
import { SessionEventWriter } from "./session-event-writer.ts";
|
|
9
9
|
export interface MultiSessionHostOptions {
|
|
10
|
+
/** Host deployment policy; never derived from client capabilities or project settings. */
|
|
11
|
+
mcpPresenterEnforcement?: boolean;
|
|
12
|
+
presenterBootstrapFd?: number;
|
|
10
13
|
agentDir: string;
|
|
11
14
|
createRuntime: CreateAgentSessionRuntimeFactory;
|
|
12
15
|
workerConfiguration?: CliRuntimeConfiguration;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
2
4
|
import { basename, dirname, join } from "node:path";
|
|
3
5
|
import { envValue } from "../../core/brand.js";
|
|
4
6
|
import { HostMcpRegistry } from "../../core/extensions/builtin/mcp/host-registry.js";
|
|
@@ -36,6 +38,8 @@ import { formatSocketAddress, listen, resolveSocketPath } from "./host-socket-bi
|
|
|
36
38
|
import { socketSink } from "./socket-sink.js";
|
|
37
39
|
import { ensureSocketSecret, SOCKET_SECRET_FILE_ENV, socketSecretPath } from "./socket-transport.js";
|
|
38
40
|
import { WorkerSessionRegistry } from "./worker-session-registry.js";
|
|
41
|
+
import { McpPresenterControl } from "./mcp-presenter-control.js";
|
|
42
|
+
import { presenterBootstrapStream } from "./mcp-presenter-bootstrap.js";
|
|
39
43
|
export { DEFAULT_HOST_EMPTY_EXIT_MS, DEFAULT_SESSION_IDLE_EVICTION_MS, RPC_HOST_EMPTY_EXIT_MS_ENV, RPC_SESSION_IDLE_EVICTION_MS_ENV, resolveHostIdlePolicy, } from "./host-idle-policy.js";
|
|
40
44
|
/** Win32 named-pipe close can leave libuv's server callback pending after handles are destroyed. */
|
|
41
45
|
const WINDOWS_SHUTDOWN_HARD_EXIT_MS = 2_000;
|
|
@@ -58,6 +62,8 @@ export async function runMultiSessionHost(options) {
|
|
|
58
62
|
: runSocketHost(options, resolveSocketPath(options.listen, options.agentDir)));
|
|
59
63
|
}
|
|
60
64
|
export function createHostCore(options, writer, capabilities = parseClientCapabilities(envValue("RPC_CLIENT_CAPABILITIES")), idle = {}, hostContext, hooks = {}) {
|
|
65
|
+
if (options.mcpPresenterEnforcement === true)
|
|
66
|
+
writer.enableMcpPresenters();
|
|
61
67
|
const policy = resolveHostIdlePolicy(process.env, idle);
|
|
62
68
|
const endpointSocket = publicEndpointSocket(options);
|
|
63
69
|
const pathReservations = createEndpointReservations({
|
|
@@ -126,9 +132,11 @@ export function createHostCore(options, writer, capabilities = parseClientCapabi
|
|
|
126
132
|
parsed = JSON.parse(line);
|
|
127
133
|
}
|
|
128
134
|
catch (cause) {
|
|
129
|
-
await writer.enqueueControl(parseError(`Failed to parse command: ${errorMessage(cause)}`));
|
|
135
|
+
await writer.enqueueControl(parseError(writer.mcpPresenters ? "Invalid command" : `Failed to parse command: ${errorMessage(cause)}`));
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
138
|
+
if (writer.handleMcpForm(parsed))
|
|
139
|
+
return;
|
|
132
140
|
if (await gate.intercept(parsed, (answer) => writer.enqueueControl(answer)))
|
|
133
141
|
return;
|
|
134
142
|
const shapeError = rpcCommandShapeError(parsed);
|
|
@@ -248,6 +256,7 @@ async function runSocketHost(options, socketPath) {
|
|
|
248
256
|
runtimeBuildId,
|
|
249
257
|
}),
|
|
250
258
|
perform: (request) => performIdleHandover({
|
|
259
|
+
presenterBootstrap: presenterControl?.successorBootstrap(),
|
|
251
260
|
request,
|
|
252
261
|
socket: hostWatchdogBinding()?.publicSocket ?? socketPath,
|
|
253
262
|
agentDir: options.agentDir,
|
|
@@ -256,6 +265,29 @@ async function runSocketHost(options, socketPath) {
|
|
|
256
265
|
},
|
|
257
266
|
}),
|
|
258
267
|
});
|
|
268
|
+
let presenterDir;
|
|
269
|
+
let presenterControl;
|
|
270
|
+
if (options.presenterBootstrapFd !== undefined) {
|
|
271
|
+
const bootstrap = presenterBootstrapStream(options.presenterBootstrapFd);
|
|
272
|
+
try {
|
|
273
|
+
if (bootstrap && options.mcpPresenterEnforcement === true) {
|
|
274
|
+
if (process.platform !== "win32")
|
|
275
|
+
presenterDir = await mkdtemp(join(tmpdir(), "senpi-presenter-"));
|
|
276
|
+
presenterControl = new McpPresenterControl(writer, presenterDir
|
|
277
|
+
? join(presenterDir, "control.sock")
|
|
278
|
+
: `\\\\.\\pipe\\senpi-presenter-${writer.mcpPresenters?.instance}`);
|
|
279
|
+
await presenterControl.listen();
|
|
280
|
+
presenterControl.bootstrap(bootstrap);
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
bootstrap?.destroy();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
bootstrap?.destroy();
|
|
287
|
+
presenterControl?.dispose();
|
|
288
|
+
presenterControl = undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
259
291
|
const observers = startHostObservers(router, writer, {
|
|
260
292
|
trim,
|
|
261
293
|
// The shape #1893 measured: gigabytes resident with `sessions.total 0`. Say it once, and when
|
|
@@ -363,6 +395,9 @@ async function runSocketHost(options, socketPath) {
|
|
|
363
395
|
shuttingDown = true;
|
|
364
396
|
observers.stop();
|
|
365
397
|
handover?.dispose();
|
|
398
|
+
presenterControl?.dispose();
|
|
399
|
+
if (presenterDir)
|
|
400
|
+
await rm(presenterDir, { recursive: true, force: true });
|
|
366
401
|
stopChildReaper();
|
|
367
402
|
// A handover that completed at once drains this host; its caller still gets the answer.
|
|
368
403
|
await handoverAnswered();
|
|
@@ -74,6 +74,7 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
|
|
|
74
74
|
workerSecurityBridge: writer.workerSecurityBridge,
|
|
75
75
|
grantDialogSettled: (id) => writer.settleGrantDialog(sessionId, id),
|
|
76
76
|
bindSecurityPresentation: (session, ui) => writer.bindSecurityPresentations(sessionId, session.sessionId, session.securityNotices, ui),
|
|
77
|
+
bindMcpForms: (session, ui) => writer.bindMcpForms(sessionId, session.sessionId, ui, () => session.admitHumanAnswer()),
|
|
77
78
|
clientInfo: options.clientInfo === undefined
|
|
78
79
|
? undefined
|
|
79
80
|
: {
|
|
@@ -84,7 +85,11 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
|
|
|
84
85
|
});
|
|
85
86
|
await handler.ready;
|
|
86
87
|
return {
|
|
87
|
-
handle: (command) =>
|
|
88
|
+
handle: (command) => {
|
|
89
|
+
if (writer.handleMcpForm(command))
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
return runWithProviderScope(entry.scope, () => handler.handleInputLine(JSON.stringify(command)));
|
|
92
|
+
},
|
|
88
93
|
cancelPendingExtensionUiRequests: () => runWithProviderScope(entry.scope, () => handler.cancelPendingExtensionUiRequests()),
|
|
89
94
|
pendingPrompts: () => handler.pendingPrompts(),
|
|
90
95
|
connectionDetached: (connectionId) => runWithProviderScope(entry.scope, () => handler.connectionDetached(connectionId)),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionUIContext, SessionKind } from "../../core/extensions/types.ts";
|
|
2
|
+
import { type McpFormBinding } from "../../core/mcp-form-types.ts";
|
|
3
|
+
import { McpPresenterAuthority, type McpPresenterConnection } from "./mcp-presenter-authority.ts";
|
|
2
4
|
import type { SecurityNoticeSnapshot, SecurityNoticeState } from "../../core/security-notices.ts";
|
|
3
5
|
import { type MediaPersister } from "./media-placeholders.ts";
|
|
4
6
|
import type { RpcHostLifecycleEvent, RpcOpenQueuedEvent, RpcSessionClosedEvent, RpcSessionClosedReason, RpcSessionParkedEvent } from "./rpc-types.ts";
|
|
@@ -21,6 +23,18 @@ export declare const MAX_SHARED_STDIO_QUEUE_RECORDS = 4096;
|
|
|
21
23
|
* scheduling boundary and violate D9.
|
|
22
24
|
*/
|
|
23
25
|
export declare class SessionEventWriter {
|
|
26
|
+
private presenterAuthority?;
|
|
27
|
+
private readonly presenterConnections;
|
|
28
|
+
private readonly presenterBindings;
|
|
29
|
+
/** Deployment switch. Ordinary RPC behavior is unchanged unless the host explicitly enables it. */
|
|
30
|
+
enableMcpPresenters(): McpPresenterAuthority;
|
|
31
|
+
get mcpPresenters(): McpPresenterAuthority | undefined;
|
|
32
|
+
getMcpPresenterConnection(id: string): McpPresenterConnection | undefined;
|
|
33
|
+
findMcpPresenterConnection(nonce: string): McpPresenterConnection | undefined;
|
|
34
|
+
getMcpFormBinding(sessionId: string): McpFormBinding | undefined;
|
|
35
|
+
bindMcpForms(sessionId: string, durableId: string, ui: object, admit: () => void): (() => void) | undefined | Promise<(() => void) | undefined>;
|
|
36
|
+
/** Private records bypass event queues, snapshots, mirrors, replay, and overflow preview errors. */
|
|
37
|
+
handleMcpForm(value: unknown): boolean;
|
|
24
38
|
private readonly records;
|
|
25
39
|
private readonly queues;
|
|
26
40
|
private readonly fanout;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { bindPrivateMcpForms } from "../../core/mcp-form-types.js";
|
|
3
|
+
import { McpPresenterAuthority } from "./mcp-presenter-authority.js";
|
|
2
4
|
import { MEDIA_PLACEHOLDERS_CAPABILITY } from "./custom-capability.js";
|
|
3
5
|
import { serializeJsonLine } from "./jsonl.js";
|
|
4
6
|
import { omitInlineMedia } from "./media-placeholders.js";
|
|
@@ -58,6 +60,79 @@ function toolUpdateKey(value) {
|
|
|
58
60
|
* scheduling boundary and violate D9.
|
|
59
61
|
*/
|
|
60
62
|
export class SessionEventWriter {
|
|
63
|
+
/** Deployment switch. Ordinary RPC behavior is unchanged unless the host explicitly enables it. */
|
|
64
|
+
enableMcpPresenters() {
|
|
65
|
+
this.presenterAuthority ??= new McpPresenterAuthority();
|
|
66
|
+
return this.presenterAuthority;
|
|
67
|
+
}
|
|
68
|
+
get mcpPresenters() {
|
|
69
|
+
return this.presenterAuthority;
|
|
70
|
+
}
|
|
71
|
+
getMcpPresenterConnection(id) {
|
|
72
|
+
return this.presenterConnections.get(id);
|
|
73
|
+
}
|
|
74
|
+
findMcpPresenterConnection(nonce) {
|
|
75
|
+
return [...this.presenterConnections.values()].find((connection) => connection.nonce === nonce);
|
|
76
|
+
}
|
|
77
|
+
getMcpFormBinding(sessionId) {
|
|
78
|
+
return this.presenterBindings.get(sessionId);
|
|
79
|
+
}
|
|
80
|
+
bindMcpForms(sessionId, durableId, ui, admit) {
|
|
81
|
+
const authority = this.presenterAuthority;
|
|
82
|
+
if (!authority)
|
|
83
|
+
return undefined;
|
|
84
|
+
const binding = authority.bind(sessionId, durableId, admit);
|
|
85
|
+
this.presenterBindings.set(sessionId, binding);
|
|
86
|
+
for (const id of this.fanout.attachedConnections(sessionId)) {
|
|
87
|
+
const connection = this.presenterConnections.get(id);
|
|
88
|
+
if (connection)
|
|
89
|
+
authority.attach(connection, binding);
|
|
90
|
+
}
|
|
91
|
+
bindPrivateMcpForms(ui, binding);
|
|
92
|
+
return () => {
|
|
93
|
+
binding.dispose();
|
|
94
|
+
if (this.presenterBindings.get(sessionId) === binding)
|
|
95
|
+
this.presenterBindings.delete(sessionId);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Private records bypass event queues, snapshots, mirrors, replay, and overflow preview errors. */
|
|
99
|
+
handleMcpForm(value) {
|
|
100
|
+
if (!this.presenterAuthority)
|
|
101
|
+
return false;
|
|
102
|
+
if (typeof value !== "object" ||
|
|
103
|
+
value === null ||
|
|
104
|
+
!("type" in value) ||
|
|
105
|
+
typeof value.type !== "string" ||
|
|
106
|
+
(!value.type.startsWith("mcp_form_") && value.type !== "mcp_presenter_context"))
|
|
107
|
+
return false;
|
|
108
|
+
const id = this.currentConnection();
|
|
109
|
+
const sender = id === undefined ? undefined : this.presenterConnections.get(id);
|
|
110
|
+
const context = value.type === "mcp_presenter_context" && "sessionId" in value && typeof value.sessionId === "string" && sender
|
|
111
|
+
? this.presenterAuthority?.connectionContext(sender, value.sessionId)
|
|
112
|
+
: undefined;
|
|
113
|
+
const reply = value.type === "mcp_presenter_context"
|
|
114
|
+
? context
|
|
115
|
+
? { ok: true, context }
|
|
116
|
+
: { error: "presenter_not_authorized" }
|
|
117
|
+
: sender && this.presenterAuthority
|
|
118
|
+
? this.presenterAuthority.receive(sender, value)
|
|
119
|
+
: { error: "presenter_not_authorized" };
|
|
120
|
+
const connection = id === undefined ? undefined : this.fanout.get(id)?.connection;
|
|
121
|
+
if (connection) {
|
|
122
|
+
try {
|
|
123
|
+
connection.writeRaw(serializeJsonLine({
|
|
124
|
+
type: "mcp_form_reply",
|
|
125
|
+
...("id" in value && typeof value.id === "string" ? { id: value.id } : {}),
|
|
126
|
+
...reply,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
if (sender)
|
|
131
|
+
this.presenterAuthority?.detach(sender);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
61
136
|
get pendingCloseRecordCount() {
|
|
62
137
|
return this.reservedCloseRecords;
|
|
63
138
|
}
|
|
@@ -65,6 +140,8 @@ export class SessionEventWriter {
|
|
|
65
140
|
return this.reservedCloseBytes;
|
|
66
141
|
}
|
|
67
142
|
constructor(writeRaw, waitOrSchedule, scheduleFlush) {
|
|
143
|
+
this.presenterConnections = new Map();
|
|
144
|
+
this.presenterBindings = new Map();
|
|
68
145
|
this.records = new SessionRecordAccounting();
|
|
69
146
|
this.queues = new Map();
|
|
70
147
|
this.fanout = new SessionEventFanout();
|
|
@@ -99,7 +176,13 @@ export class SessionEventWriter {
|
|
|
99
176
|
this.closeOverflowActors = new WeakSet();
|
|
100
177
|
this.reservedCloseRecords = 0;
|
|
101
178
|
this.reservedCloseBytes = 0;
|
|
102
|
-
this.fanout.onConnectionClosed = (id) =>
|
|
179
|
+
this.fanout.onConnectionClosed = (id) => {
|
|
180
|
+
this.securityPresentations.dropConnection(id);
|
|
181
|
+
const sender = this.presenterConnections.get(id);
|
|
182
|
+
if (sender)
|
|
183
|
+
this.presenterAuthority?.detach(sender);
|
|
184
|
+
this.presenterConnections.delete(id);
|
|
185
|
+
};
|
|
103
186
|
this.writeRaw = writeRaw;
|
|
104
187
|
if (waitOrSchedule && scheduleFlush === undefined && waitOrSchedule.length > 0) {
|
|
105
188
|
this.scheduleFlush = waitOrSchedule;
|
|
@@ -119,9 +202,33 @@ export class SessionEventWriter {
|
|
|
119
202
|
return this.sealedSessions.has(sessionId);
|
|
120
203
|
}
|
|
121
204
|
registerConnection(id, connection, options = {}) {
|
|
205
|
+
const previousPresenter = this.presenterConnections.get(id);
|
|
206
|
+
if (previousPresenter)
|
|
207
|
+
this.presenterAuthority?.detach(previousPresenter);
|
|
122
208
|
this.fanout.registerConnection(id, connection, options);
|
|
209
|
+
if (this.presenterAuthority) {
|
|
210
|
+
const sender = this.presenterAuthority.connect(id, (frame) => {
|
|
211
|
+
if (!this.fanout.get(id))
|
|
212
|
+
return false;
|
|
213
|
+
try {
|
|
214
|
+
connection.writeRaw(serializeJsonLine(frame));
|
|
215
|
+
// A pending drain does not undo an accepted write or drop the next resolution.
|
|
216
|
+
void connection.waitForBackpressure().catch(() => this.presenterAuthority?.detach(sender));
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
this.presenterAuthority?.detach(sender);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
this.presenterConnections.set(id, sender);
|
|
225
|
+
}
|
|
123
226
|
}
|
|
124
227
|
unregisterConnection(id) {
|
|
228
|
+
const sender = this.presenterConnections.get(id);
|
|
229
|
+
if (sender)
|
|
230
|
+
this.presenterAuthority?.detach(sender);
|
|
231
|
+
this.presenterConnections.delete(id);
|
|
125
232
|
this.securityPresentations.dropConnection(id);
|
|
126
233
|
this.fanout.unregisterConnection(id);
|
|
127
234
|
this.status.dropConnection(id);
|
|
@@ -131,8 +238,16 @@ export class SessionEventWriter {
|
|
|
131
238
|
this.fanout.attachConnectionToSession(id, sessionId);
|
|
132
239
|
if (this.hasConnection(id))
|
|
133
240
|
this.securityPresentations.attach(id, sessionId);
|
|
241
|
+
const sender = this.presenterConnections.get(id);
|
|
242
|
+
const binding = this.presenterBindings.get(sessionId);
|
|
243
|
+
if (sender && binding)
|
|
244
|
+
this.presenterAuthority?.attach(sender, binding);
|
|
134
245
|
}
|
|
135
246
|
detachConnectionFromSession(id, sessionId) {
|
|
247
|
+
const sender = this.presenterConnections.get(id);
|
|
248
|
+
const binding = this.presenterBindings.get(sessionId);
|
|
249
|
+
if (sender && binding)
|
|
250
|
+
this.presenterAuthority?.detach(sender, binding);
|
|
136
251
|
this.fanout.detachConnectionFromSession(id, sessionId);
|
|
137
252
|
this.securityPresentations.detach(id, sessionId);
|
|
138
253
|
}
|
|
@@ -142,8 +257,16 @@ export class SessionEventWriter {
|
|
|
142
257
|
setConnectionCapabilities(id, capabilities) {
|
|
143
258
|
this.fanout.setConnectionCapabilities(id, capabilities);
|
|
144
259
|
this.securityPresentations.capabilitiesChanged(id);
|
|
260
|
+
if (!capabilities.includes("interactive_user") || !capabilities.includes("mcp_form_presenter_v1")) {
|
|
261
|
+
const sender = this.presenterConnections.get(id);
|
|
262
|
+
if (sender)
|
|
263
|
+
this.presenterAuthority?.withdraw(sender);
|
|
264
|
+
}
|
|
145
265
|
}
|
|
146
266
|
clearConnectionCapabilities(id) {
|
|
267
|
+
const sender = this.presenterConnections.get(id);
|
|
268
|
+
if (sender)
|
|
269
|
+
this.presenterAuthority?.withdraw(sender);
|
|
147
270
|
this.fanout.clearConnectionCapabilities(id);
|
|
148
271
|
this.securityPresentations.capabilitiesChanged(id);
|
|
149
272
|
}
|
|
@@ -459,6 +582,8 @@ export class SessionEventWriter {
|
|
|
459
582
|
* session's final stdout record.
|
|
460
583
|
*/
|
|
461
584
|
closeSession(sessionId, response, reason, sessionPath) {
|
|
585
|
+
this.presenterBindings.get(sessionId)?.dispose();
|
|
586
|
+
this.presenterBindings.delete(sessionId);
|
|
462
587
|
if (!this.settleBeforeSeal(sessionId))
|
|
463
588
|
return;
|
|
464
589
|
this.sealedSessions.add(sessionId);
|
|
@@ -526,6 +651,8 @@ export class SessionEventWriter {
|
|
|
526
651
|
});
|
|
527
652
|
}
|
|
528
653
|
sealLifecycle(sessionId, lifecycle, attachedOnly = false) {
|
|
654
|
+
this.presenterBindings.get(sessionId)?.dispose();
|
|
655
|
+
this.presenterBindings.delete(sessionId);
|
|
529
656
|
if (!this.settleBeforeSeal(sessionId))
|
|
530
657
|
return;
|
|
531
658
|
this.sealedSessions.add(sessionId);
|
|
@@ -69,6 +69,7 @@ export async function dispatch(host, command) {
|
|
|
69
69
|
warm: host.registry.warm !== undefined,
|
|
70
70
|
toolRuntime: host.defaults.toolRuntime ?? "in-process",
|
|
71
71
|
negotiated: host.connectionOptions?.capabilities ?? [],
|
|
72
|
+
mcpPresenters: host.writer.mcpPresenters !== undefined,
|
|
72
73
|
});
|
|
73
74
|
return {
|
|
74
75
|
id: command.id,
|