@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.
Files changed (226) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/bundle/chunks/{args-JXULI4EH.js → args-B444OQBK.js} +1 -1
  3. package/dist/bundle/chunks/{auth-4NUNUSTF.js → auth-C77UQA55.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
  5. package/dist/bundle/chunks/chunk-6O4SEW2W.js +5 -0
  6. package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-YDCRA5ES.js → chunk-7W7OJDIY.js} +4 -4
  8. package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
  9. package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-C5UFJ7B3.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-CNL6QSUB.js} +4 -4
  11. package/dist/bundle/chunks/{chunk-BTNYLNWZ.js → chunk-FFQOEC76.js} +5 -5
  12. package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
  13. package/dist/bundle/chunks/chunk-H3HNMY2W.js +13 -0
  14. package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-HVJI5T75.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-JA2R2ZSE.js} +1 -1
  16. package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
  17. package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
  18. package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
  19. package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
  20. package/dist/bundle/chunks/{chunk-EYBDBR7V.js → chunk-MJ5NNWX4.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-O3Q2ET6W.js} +2 -5
  22. package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
  23. package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-ODTGC6YU.js} +1 -1
  24. package/dist/bundle/chunks/chunk-P3M2PF6K.js +27 -0
  25. package/dist/bundle/chunks/chunk-SWJXRC2I.js +5 -0
  26. package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
  27. package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-UQNRW3VU.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
  30. package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
  31. package/dist/bundle/chunks/{chunk-7WAJCESX.js → chunk-WLHNEUGD.js} +1 -1
  32. package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
  33. package/dist/bundle/chunks/{chunk-URBJS24R.js → chunk-YRF4I43T.js} +35 -35
  34. package/dist/bundle/chunks/chunk-Z3UYNSVR.js +3 -0
  35. package/dist/bundle/chunks/cli-main-HEKFFB5S.js +3 -0
  36. package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
  37. package/dist/bundle/chunks/dispatch-NYAULNVW.js +9 -0
  38. package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
  39. package/dist/bundle/chunks/host-command-Z3WPHJOW.js +2 -0
  40. package/dist/bundle/chunks/host-lifecycle-ONQIMWMX.js +3 -0
  41. package/dist/bundle/chunks/host-lifecycle.js +9 -7
  42. package/dist/bundle/chunks/host-session-command-J65ENSV5.js +20 -0
  43. package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-3CWE4265.js} +1 -1
  44. package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +2 -0
  45. package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
  46. package/dist/bundle/chunks/{multi-session-host-M7HVUMRH.js → multi-session-host-YI2WZOET.js} +12 -9
  47. package/dist/bundle/chunks/{package-manager-cli-FSR5P6YL.js → package-manager-cli-GHZPJMRF.js} +1 -1
  48. package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
  49. package/dist/bundle/chunks/{rotation-stream-U7YQMC7H.js → rotation-stream-ZPVCFOVD.js} +1 -1
  50. package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +2 -0
  51. package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-FFEQS744.js} +1 -1
  52. package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
  53. package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-YWG6D6ZF.js} +1 -1
  54. package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +3 -0
  55. package/dist/bundle/chunks/{session-picker-KQUFWDQC.js → session-picker-25H5KNYP.js} +1 -1
  56. package/dist/bundle/chunks/session-worker.js +285 -271
  57. package/dist/bundle/chunks/{stdio-IEC635SY.js → stdio-YXDKKKQZ.js} +1 -1
  58. package/dist/bundle/chunks/{streamableHttp-KJI5KE4K.js → streamableHttp-DKIBUWVY.js} +1 -1
  59. package/dist/bundle/chunks/{types-LLNV23LW.js → types-BJUEQ4II.js} +1 -1
  60. package/dist/bundle/chunks/zod-compat-RJDXCU7S.js +2 -0
  61. package/dist/bundle/cli.js +2 -2
  62. package/dist/bundle/host-supervisor.js +9 -7
  63. package/dist/bundle/index.js +1 -1
  64. package/dist/bundle/rpc-entry.js +1 -1
  65. package/dist/bundle/runtime-manifest.json +1 -1
  66. package/dist/cli/args.d.ts +2 -0
  67. package/dist/cli/args.js +9 -0
  68. package/dist/cli/host-command-args.d.ts +1 -0
  69. package/dist/cli/host-command-args.js +7 -1
  70. package/dist/cli/host-command.js +3 -1
  71. package/dist/core/agent-session.d.ts +1 -0
  72. package/dist/core/agent-session.js +50 -16
  73. package/dist/core/engine-turn-limit.d.ts +2 -0
  74. package/dist/core/engine-turn-limit.js +7 -1
  75. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
  76. package/dist/core/extensions/builtin/goal/index.js +1 -0
  77. package/dist/core/extensions/builtin/goal/lifecycle-facts.d.ts +3 -0
  78. package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
  79. package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
  80. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  81. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  82. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  83. package/dist/core/extensions/builtin/mcp/connection.js +46 -1
  84. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +6 -1
  85. package/dist/core/extensions/builtin/mcp/elicitation.js +56 -2
  86. package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
  87. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
  88. package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
  89. package/dist/core/extensions/builtin/mcp/health.js +5 -0
  90. package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
  91. package/dist/core/extensions/builtin/mcp/index.js +2 -0
  92. package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
  93. package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
  94. package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
  95. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
  96. package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
  97. package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
  98. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
  99. package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
  100. package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
  101. package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
  102. package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
  103. package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
  104. package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
  105. package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
  106. package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
  107. package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
  108. package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
  109. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
  110. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
  111. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
  112. package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
  113. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
  114. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
  115. package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
  116. package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
  117. package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
  118. package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
  119. package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
  120. package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
  121. package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
  122. package/dist/core/extensions/builtin/mcp/resources.js +19 -4
  123. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
  124. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
  125. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
  126. package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
  127. package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
  128. package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
  129. package/dist/core/extensions/builtin/mcp/service.js +79 -4
  130. package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
  131. package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
  132. package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
  133. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
  134. package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
  135. package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
  136. package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
  137. package/dist/core/extensions/runner.js +3 -2
  138. package/dist/core/extensions/types.d.ts +2 -0
  139. package/dist/core/lifecycle-ledger.d.ts +1 -0
  140. package/dist/core/lifecycle-ledger.js +7 -2
  141. package/dist/core/lifecycle-record.js +3 -0
  142. package/dist/core/lifecycle-runtime-facts.js +3 -0
  143. package/dist/core/lifecycle-session-binding.d.ts +11 -1
  144. package/dist/core/lifecycle-session-binding.js +35 -4
  145. package/dist/core/lifecycle-stop.d.ts +41 -0
  146. package/dist/core/lifecycle-stop.js +92 -0
  147. package/dist/core/mcp-form-types.d.ts +89 -0
  148. package/dist/core/mcp-form-types.js +23 -0
  149. package/dist/core/mcp-mrtr-presentations.d.ts +15 -0
  150. package/dist/core/mcp-mrtr-presentations.js +17 -0
  151. package/dist/core/run-resume-turn.d.ts +1 -0
  152. package/dist/core/run-resume-turn.js +4 -3
  153. package/dist/core/session-lifecycle-types.d.ts +15 -0
  154. package/dist/core/session-lifecycle.d.ts +17 -1
  155. package/dist/core/session-lifecycle.js +40 -2
  156. package/dist/core/stop-resume-directive.d.ts +1 -1
  157. package/dist/core/stop-resume-directive.js +10 -1
  158. package/dist/main.js +5 -1
  159. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  160. package/dist/modes/interactive/components/extension-selector.js +4 -0
  161. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  162. package/dist/modes/interactive/interactive-mode.js +35 -8
  163. package/dist/modes/interactive/mcp-local-presenter.d.ts +5 -0
  164. package/dist/modes/interactive/mcp-local-presenter.js +109 -0
  165. package/dist/modes/interactive/pause-footer.d.ts +5 -0
  166. package/dist/modes/interactive/pause-footer.js +2 -0
  167. package/dist/modes/rpc/connection-handler.d.ts +1 -0
  168. package/dist/modes/rpc/connection-handler.js +7 -1
  169. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  170. package/dist/modes/rpc/host-capabilities.js +2 -1
  171. package/dist/modes/rpc/host-ensure-start.js +13 -2
  172. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  173. package/dist/modes/rpc/host-ensure.js +1 -0
  174. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  175. package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
  176. package/dist/modes/rpc/host-handover-wire.js +1 -0
  177. package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
  178. package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
  179. package/dist/modes/rpc/host-reservations.js +3 -2
  180. package/dist/modes/rpc/host-runner.d.ts +2 -0
  181. package/dist/modes/rpc/host-runner.js +8 -6
  182. package/dist/modes/rpc/host-stalled-evidence.js +2 -1
  183. package/dist/modes/rpc/host-successor.js +11 -1
  184. package/dist/modes/rpc/mcp-form-dialog.d.ts +45 -0
  185. package/dist/modes/rpc/mcp-form-dialog.js +159 -0
  186. package/dist/modes/rpc/mcp-form-worker.d.ts +14 -0
  187. package/dist/modes/rpc/mcp-form-worker.js +167 -0
  188. package/dist/modes/rpc/mcp-presenter-authority.d.ts +53 -0
  189. package/dist/modes/rpc/mcp-presenter-authority.js +309 -0
  190. package/dist/modes/rpc/mcp-presenter-bootstrap.d.ts +8 -0
  191. package/dist/modes/rpc/mcp-presenter-bootstrap.js +111 -0
  192. package/dist/modes/rpc/mcp-presenter-control.d.ts +24 -0
  193. package/dist/modes/rpc/mcp-presenter-control.js +330 -0
  194. package/dist/modes/rpc/multi-session-host.d.ts +3 -0
  195. package/dist/modes/rpc/multi-session-host.js +36 -1
  196. package/dist/modes/rpc/session-binding.js +6 -1
  197. package/dist/modes/rpc/session-event-writer.d.ts +14 -0
  198. package/dist/modes/rpc/session-event-writer.js +128 -1
  199. package/dist/modes/rpc/session-router-routing.js +1 -0
  200. package/dist/modes/rpc/session-worker-client.d.ts +3 -0
  201. package/dist/modes/rpc/session-worker-client.js +89 -0
  202. package/dist/modes/rpc/session-worker-protocol.d.ts +41 -0
  203. package/dist/modes/rpc/session-worker.js +9 -0
  204. package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
  205. package/dist/utils/process-identity.d.ts +7 -0
  206. package/dist/utils/process-identity.js +32 -5
  207. package/docs/extensions.md +5 -0
  208. package/docs/mcp.md +20 -3
  209. package/docs/rpc.md +111 -0
  210. package/package.json +6 -6
  211. package/dist/bundle/chunks/chunk-3VOG6WFU.js +0 -27
  212. package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
  213. package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
  214. package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
  215. package/dist/bundle/chunks/chunk-II22E3CV.js +0 -3
  216. package/dist/bundle/chunks/chunk-J5P2VL3Y.js +0 -5
  217. package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
  218. package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
  219. package/dist/bundle/chunks/cli-main-TABBP55A.js +0 -3
  220. package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
  221. package/dist/bundle/chunks/host-command-OQR4RDTM.js +0 -2
  222. package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +0 -3
  223. package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +0 -20
  224. package/dist/bundle/chunks/interactive-mode-DGQNSXUA.js +0 -2
  225. package/dist/bundle/chunks/rpc-mode-SZAJNKKL.js +0 -2
  226. 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) => runWithProviderScope(entry.scope, () => handler.handleInputLine(JSON.stringify(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) => this.securityPresentations.dropConnection(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,