@code-yeongyu/senpi 2026.10.25-2 → 2026.10.25-4

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 +37 -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-XBUSVKWC.js → chunk-2RB5EL4X.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-KUE4AQN6.js → chunk-3QCYBOKN.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-2EFVQWXZ.js → chunk-4MOZXOJX.js} +5 -5
  7. package/dist/bundle/chunks/{chunk-MXZPAUFB.js → chunk-5JJDR6XI.js} +1 -1
  8. package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
  9. package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
  10. package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
  11. package/dist/bundle/chunks/{chunk-VNN2V74Y.js → chunk-BZ7726QO.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-J2U5VDG7.js → chunk-C2MQ4QXN.js} +30 -30
  13. package/dist/bundle/chunks/{chunk-JRXKLIEQ.js → chunk-DODH3N6U.js} +5 -5
  14. package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-E6D4RIT7.js} +2 -2
  15. package/dist/bundle/chunks/chunk-J37GRED2.js +5 -0
  16. package/dist/bundle/chunks/chunk-JMOUU75H.js +2 -0
  17. package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
  18. package/dist/bundle/chunks/chunk-KKD4CYCI.js +3 -0
  19. package/dist/bundle/chunks/{chunk-YN7ELQ43.js → chunk-KNVNKDER.js} +1 -1
  20. package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
  21. package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
  22. package/dist/bundle/chunks/{chunk-G2UETS2R.js → chunk-LOIF6PHI.js} +4 -4
  23. package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
  24. package/dist/bundle/chunks/chunk-N7IN46Q5.js +27 -0
  25. package/dist/bundle/chunks/{chunk-LYGAV6KU.js → chunk-PT6JJUN2.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-WFXUCVIR.js → chunk-SBGIXMX3.js} +2 -5
  27. package/dist/bundle/chunks/{chunk-W2BKZBYJ.js → chunk-STMXIBXF.js} +2 -2
  28. package/dist/bundle/chunks/chunk-TBU6VYHN.js +13 -0
  29. package/dist/bundle/chunks/{chunk-RXKNOEQQ.js → chunk-V2UYXTRR.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
  31. package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
  32. package/dist/bundle/chunks/chunk-XMJKZK5H.js +2 -0
  33. package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
  34. package/dist/bundle/chunks/cli-main-YYH3IBC6.js +3 -0
  35. package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
  36. package/dist/bundle/chunks/dispatch-MXG5YWLV.js +9 -0
  37. package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
  38. package/dist/bundle/chunks/host-command-TDLLHXVI.js +2 -0
  39. package/dist/bundle/chunks/host-lifecycle-3VHHIGDL.js +3 -0
  40. package/dist/bundle/chunks/host-lifecycle.js +8 -6
  41. package/dist/bundle/chunks/host-session-command-56XF6ACS.js +20 -0
  42. package/dist/bundle/chunks/{host-socket-request-AS2OCRLP.js → host-socket-request-QL2WEZTN.js} +1 -1
  43. package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +2 -0
  44. package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
  45. package/dist/bundle/chunks/{multi-session-host-VTTY5MFT.js → multi-session-host-Z2CW4SDM.js} +12 -9
  46. package/dist/bundle/chunks/{package-manager-cli-DZFOC3MF.js → package-manager-cli-N55U6PEO.js} +1 -1
  47. package/dist/bundle/chunks/rotation-stream-CBGCBYHV.js +2 -0
  48. package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +2 -0
  49. package/dist/bundle/chunks/{rpc-monitor-source-B5NQ4ZVH.js → rpc-monitor-source-AE3C6EQV.js} +1 -1
  50. package/dist/bundle/chunks/{schedule-command-SLISTG4V.js → schedule-command-VS7CZC2J.js} +1 -1
  51. package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +3 -0
  52. package/dist/bundle/chunks/session-picker-E4KUJDLO.js +2 -0
  53. package/dist/bundle/chunks/session-worker.js +292 -278
  54. package/dist/bundle/chunks/{stdio-IEC635SY.js → stdio-YXDKKKQZ.js} +1 -1
  55. package/dist/bundle/chunks/{streamableHttp-KJI5KE4K.js → streamableHttp-DKIBUWVY.js} +1 -1
  56. package/dist/bundle/chunks/{types-LLNV23LW.js → types-BJUEQ4II.js} +1 -1
  57. package/dist/bundle/chunks/zod-compat-RJDXCU7S.js +2 -0
  58. package/dist/bundle/cli.js +2 -2
  59. package/dist/bundle/host-supervisor.js +8 -6
  60. package/dist/bundle/index.js +1 -1
  61. package/dist/bundle/rpc-entry.js +1 -1
  62. package/dist/bundle/runtime-manifest.json +1 -1
  63. package/dist/cli/args.d.ts +2 -0
  64. package/dist/cli/args.js +9 -0
  65. package/dist/cli/host-command-args.d.ts +1 -0
  66. package/dist/cli/host-command-args.js +7 -1
  67. package/dist/cli/host-command.js +3 -1
  68. package/dist/core/agent-session.js +22 -7
  69. package/dist/core/extensions/builtin/goal/index.js +1 -0
  70. package/dist/core/extensions/builtin/goal/lifecycle-facts.d.ts +3 -0
  71. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
  72. package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
  73. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  74. package/dist/core/extensions/builtin/mcp/connection.js +46 -1
  75. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +6 -1
  76. package/dist/core/extensions/builtin/mcp/elicitation.js +54 -2
  77. package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
  78. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
  79. package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
  80. package/dist/core/extensions/builtin/mcp/health.js +5 -0
  81. package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
  82. package/dist/core/extensions/builtin/mcp/index.js +2 -0
  83. package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
  84. package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
  85. package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
  86. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
  87. package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
  88. package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
  89. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
  90. package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
  91. package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
  92. package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
  93. package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
  94. package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
  95. package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
  96. package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
  97. package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
  98. package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
  99. package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
  100. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
  101. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
  102. package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
  103. package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
  104. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
  105. package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
  106. package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
  107. package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
  108. package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
  109. package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
  110. package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
  111. package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
  112. package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
  113. package/dist/core/extensions/builtin/mcp/resources.js +19 -4
  114. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
  115. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
  116. package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
  117. package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
  118. package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
  119. package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
  120. package/dist/core/extensions/builtin/mcp/service.js +79 -4
  121. package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
  122. package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
  123. package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
  124. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
  125. package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
  126. package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
  127. package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
  128. package/dist/core/extensions/runner.js +3 -2
  129. package/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/core/lifecycle-ledger.d.ts +1 -0
  131. package/dist/core/lifecycle-ledger.js +7 -2
  132. package/dist/core/lifecycle-record.js +3 -0
  133. package/dist/core/lifecycle-session-binding.d.ts +11 -1
  134. package/dist/core/lifecycle-session-binding.js +29 -2
  135. package/dist/core/lifecycle-stop.d.ts +41 -0
  136. package/dist/core/lifecycle-stop.js +92 -0
  137. package/dist/core/mcp-form-types.d.ts +89 -0
  138. package/dist/core/mcp-form-types.js +23 -0
  139. package/dist/core/mcp-mrtr-presentations.d.ts +15 -0
  140. package/dist/core/mcp-mrtr-presentations.js +17 -0
  141. package/dist/core/session-control-actions.js +0 -2
  142. package/dist/core/session-lifecycle-types.d.ts +15 -0
  143. package/dist/core/session-lifecycle.d.ts +17 -1
  144. package/dist/core/session-lifecycle.js +40 -2
  145. package/dist/main.js +5 -1
  146. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  147. package/dist/modes/interactive/components/extension-selector.js +4 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  149. package/dist/modes/interactive/interactive-mode.js +34 -8
  150. package/dist/modes/interactive/mcp-local-presenter.d.ts +5 -0
  151. package/dist/modes/interactive/mcp-local-presenter.js +109 -0
  152. package/dist/modes/interactive/session-control-attach.d.ts +20 -0
  153. package/dist/modes/interactive/session-control-attach.js +13 -0
  154. package/dist/modes/interactive/session-control-commands.js +16 -6
  155. package/dist/modes/interactive/session-control-registry.d.ts +2 -1
  156. package/dist/modes/interactive/session-control-registry.js +3 -2
  157. package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
  158. package/dist/modes/interactive/session-control-rpc-commands.js +114 -4
  159. package/dist/modes/interactive/session-control-server.js +9 -3
  160. package/dist/modes/rpc/connection-handler.d.ts +1 -0
  161. package/dist/modes/rpc/connection-handler.js +13 -41
  162. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  163. package/dist/modes/rpc/custom-capability.js +6 -0
  164. package/dist/modes/rpc/get-entries-command.d.ts +13 -0
  165. package/dist/modes/rpc/get-entries-command.js +61 -0
  166. package/dist/modes/rpc/get-entries-page.d.ts +22 -0
  167. package/dist/modes/rpc/get-entries-page.js +41 -2
  168. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  169. package/dist/modes/rpc/host-capabilities.js +6 -2
  170. package/dist/modes/rpc/host-ensure-start.js +13 -2
  171. package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
  172. package/dist/modes/rpc/host-ensure.js +1 -0
  173. package/dist/modes/rpc/host-handoff.d.ts +2 -0
  174. package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
  175. package/dist/modes/rpc/host-handover-wire.js +1 -0
  176. package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
  177. package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
  178. package/dist/modes/rpc/host-observe-request.d.ts +2 -1
  179. package/dist/modes/rpc/host-observe-request.js +4 -1
  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-successor.js +11 -1
  183. package/dist/modes/rpc/mcp-form-dialog.d.ts +45 -0
  184. package/dist/modes/rpc/mcp-form-dialog.js +159 -0
  185. package/dist/modes/rpc/mcp-form-worker.d.ts +14 -0
  186. package/dist/modes/rpc/mcp-form-worker.js +167 -0
  187. package/dist/modes/rpc/mcp-presenter-authority.d.ts +53 -0
  188. package/dist/modes/rpc/mcp-presenter-authority.js +309 -0
  189. package/dist/modes/rpc/mcp-presenter-bootstrap.d.ts +8 -0
  190. package/dist/modes/rpc/mcp-presenter-bootstrap.js +111 -0
  191. package/dist/modes/rpc/mcp-presenter-control.d.ts +24 -0
  192. package/dist/modes/rpc/mcp-presenter-control.js +330 -0
  193. package/dist/modes/rpc/multi-session-host.d.ts +3 -0
  194. package/dist/modes/rpc/multi-session-host.js +36 -1
  195. package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -0
  196. package/dist/modes/rpc/rpc-session-command-types.d.ts +8 -1
  197. package/dist/modes/rpc/session-binding.js +6 -1
  198. package/dist/modes/rpc/session-event-writer.d.ts +14 -0
  199. package/dist/modes/rpc/session-event-writer.js +128 -1
  200. package/dist/modes/rpc/session-router-routing.js +1 -0
  201. package/dist/modes/rpc/session-worker-client.d.ts +3 -0
  202. package/dist/modes/rpc/session-worker-client.js +89 -0
  203. package/dist/modes/rpc/session-worker-protocol.d.ts +41 -0
  204. package/dist/modes/rpc/session-worker.js +9 -0
  205. package/docs/extensions.md +5 -0
  206. package/docs/mcp.md +20 -3
  207. package/docs/rpc.md +151 -8
  208. package/package.json +6 -6
  209. package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
  210. package/dist/bundle/chunks/chunk-B6TPQDBW.js +0 -5
  211. package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
  212. package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
  213. package/dist/bundle/chunks/chunk-KJZAY5YH.js +0 -27
  214. package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
  215. package/dist/bundle/chunks/chunk-OI4NMCKP.js +0 -3
  216. package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
  217. package/dist/bundle/chunks/cli-main-SWKFMTZQ.js +0 -3
  218. package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
  219. package/dist/bundle/chunks/host-command-BC6UWP33.js +0 -2
  220. package/dist/bundle/chunks/host-lifecycle-TN5NEGNF.js +0 -3
  221. package/dist/bundle/chunks/host-session-command-MGVBLBRV.js +0 -20
  222. package/dist/bundle/chunks/interactive-mode-SCSROHD5.js +0 -2
  223. package/dist/bundle/chunks/rotation-stream-U7YQMC7H.js +0 -2
  224. package/dist/bundle/chunks/rpc-mode-TOXYD4XA.js +0 -2
  225. package/dist/bundle/chunks/session-control-endpoint-HFFMBALG.js +0 -3
  226. package/dist/bundle/chunks/session-picker-465FVXQG.js +0 -2
@@ -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();
@@ -120,6 +120,8 @@ export type RpcReadResponse = {
120
120
  leafId: string | null;
121
121
  hasMore?: boolean;
122
122
  nextSince?: string;
123
+ /** A `tail` page's oldest entry id while older entries remain (senpi#4267). */
124
+ next_before?: string;
123
125
  };
124
126
  } | {
125
127
  id?: string;
@@ -388,11 +388,18 @@ export type RpcSessionCommand = {
388
388
  } | {
389
389
  id?: string;
390
390
  type: "get_fork_messages";
391
- } | {
391
+ }
392
+ /**
393
+ * `since`/`before` bound the range (strictly after / strictly before an entry id); `limit` pages it forward from
394
+ * its start, `tail` backward from its end (`next_before` names the next page, senpi#4267).
395
+ */
396
+ | {
392
397
  id?: string;
393
398
  type: "get_entries";
394
399
  since?: string;
400
+ before?: string;
395
401
  limit?: number;
402
+ tail?: number;
396
403
  maxBytes?: number;
397
404
  } | {
398
405
  id?: string;
@@ -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;