@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.
- package/CHANGELOG.md +37 -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-XBUSVKWC.js → chunk-2RB5EL4X.js} +1 -1
- package/dist/bundle/chunks/{chunk-KUE4AQN6.js → chunk-3QCYBOKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2EFVQWXZ.js → chunk-4MOZXOJX.js} +5 -5
- package/dist/bundle/chunks/{chunk-MXZPAUFB.js → chunk-5JJDR6XI.js} +1 -1
- package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
- package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
- package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
- package/dist/bundle/chunks/{chunk-VNN2V74Y.js → chunk-BZ7726QO.js} +1 -1
- package/dist/bundle/chunks/{chunk-J2U5VDG7.js → chunk-C2MQ4QXN.js} +30 -30
- package/dist/bundle/chunks/{chunk-JRXKLIEQ.js → chunk-DODH3N6U.js} +5 -5
- package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-E6D4RIT7.js} +2 -2
- package/dist/bundle/chunks/chunk-J37GRED2.js +5 -0
- package/dist/bundle/chunks/chunk-JMOUU75H.js +2 -0
- package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
- package/dist/bundle/chunks/chunk-KKD4CYCI.js +3 -0
- package/dist/bundle/chunks/{chunk-YN7ELQ43.js → chunk-KNVNKDER.js} +1 -1
- package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
- package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
- package/dist/bundle/chunks/{chunk-G2UETS2R.js → chunk-LOIF6PHI.js} +4 -4
- package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
- package/dist/bundle/chunks/chunk-N7IN46Q5.js +27 -0
- package/dist/bundle/chunks/{chunk-LYGAV6KU.js → chunk-PT6JJUN2.js} +1 -1
- package/dist/bundle/chunks/{chunk-WFXUCVIR.js → chunk-SBGIXMX3.js} +2 -5
- package/dist/bundle/chunks/{chunk-W2BKZBYJ.js → chunk-STMXIBXF.js} +2 -2
- package/dist/bundle/chunks/chunk-TBU6VYHN.js +13 -0
- package/dist/bundle/chunks/{chunk-RXKNOEQQ.js → chunk-V2UYXTRR.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-XMJKZK5H.js +2 -0
- package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
- package/dist/bundle/chunks/cli-main-YYH3IBC6.js +3 -0
- package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
- package/dist/bundle/chunks/dispatch-MXG5YWLV.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-TDLLHXVI.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-3VHHIGDL.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +8 -6
- package/dist/bundle/chunks/host-session-command-56XF6ACS.js +20 -0
- package/dist/bundle/chunks/{host-socket-request-AS2OCRLP.js → host-socket-request-QL2WEZTN.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +2 -0
- package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-VTTY5MFT.js → multi-session-host-Z2CW4SDM.js} +12 -9
- package/dist/bundle/chunks/{package-manager-cli-DZFOC3MF.js → package-manager-cli-N55U6PEO.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-CBGCBYHV.js +2 -0
- package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-B5NQ4ZVH.js → rpc-monitor-source-AE3C6EQV.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-SLISTG4V.js → schedule-command-VS7CZC2J.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +3 -0
- package/dist/bundle/chunks/session-picker-E4KUJDLO.js +2 -0
- package/dist/bundle/chunks/session-worker.js +292 -278
- 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 +8 -6
- 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.js +22 -7
- 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/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 +54 -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-session-binding.d.ts +11 -1
- package/dist/core/lifecycle-session-binding.js +29 -2
- 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/session-control-actions.js +0 -2
- 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/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 +34 -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/session-control-attach.d.ts +20 -0
- package/dist/modes/interactive/session-control-attach.js +13 -0
- package/dist/modes/interactive/session-control-commands.js +16 -6
- package/dist/modes/interactive/session-control-registry.d.ts +2 -1
- package/dist/modes/interactive/session-control-registry.js +3 -2
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
- package/dist/modes/interactive/session-control-rpc-commands.js +114 -4
- package/dist/modes/interactive/session-control-server.js +9 -3
- package/dist/modes/rpc/connection-handler.d.ts +1 -0
- package/dist/modes/rpc/connection-handler.js +13 -41
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/get-entries-command.d.ts +13 -0
- package/dist/modes/rpc/get-entries-command.js +61 -0
- package/dist/modes/rpc/get-entries-page.d.ts +22 -0
- package/dist/modes/rpc/get-entries-page.js +41 -2
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +6 -2
- 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-observe-request.d.ts +2 -1
- package/dist/modes/rpc/host-observe-request.js +4 -1
- package/dist/modes/rpc/host-runner.d.ts +2 -0
- package/dist/modes/rpc/host-runner.js +8 -6
- 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/rpc-read-response-types.d.ts +2 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +8 -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/docs/extensions.md +5 -0
- package/docs/mcp.md +20 -3
- package/docs/rpc.md +151 -8
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
- package/dist/bundle/chunks/chunk-B6TPQDBW.js +0 -5
- package/dist/bundle/chunks/chunk-CDRFFHLL.js +0 -4
- package/dist/bundle/chunks/chunk-HPCS4HM4.js +0 -27
- package/dist/bundle/chunks/chunk-KJZAY5YH.js +0 -27
- package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
- package/dist/bundle/chunks/chunk-OI4NMCKP.js +0 -3
- package/dist/bundle/chunks/chunk-SCVRKIFK.js +0 -2
- package/dist/bundle/chunks/cli-main-SWKFMTZQ.js +0 -3
- package/dist/bundle/chunks/dispatch-DEP4PEDZ.js +0 -10
- package/dist/bundle/chunks/host-command-BC6UWP33.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-TN5NEGNF.js +0 -3
- package/dist/bundle/chunks/host-session-command-MGVBLBRV.js +0 -20
- package/dist/bundle/chunks/interactive-mode-SCSROHD5.js +0 -2
- package/dist/bundle/chunks/rotation-stream-U7YQMC7H.js +0 -2
- package/dist/bundle/chunks/rpc-mode-TOXYD4XA.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-HFFMBALG.js +0 -3
- package/dist/bundle/chunks/session-picker-465FVXQG.js +0 -2
|
@@ -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,
|
|
@@ -16,6 +16,9 @@ export interface SessionWorkerCallbacks {
|
|
|
16
16
|
failure: (error: string) => void;
|
|
17
17
|
}
|
|
18
18
|
export declare class SessionWorkerClient {
|
|
19
|
+
private privateBinding?;
|
|
20
|
+
private stopPrivateBinding?;
|
|
21
|
+
private readonly privateOperations;
|
|
19
22
|
/** The worker's own route to the main-thread Bun.WebView service; released when the worker exits. */
|
|
20
23
|
private readonly webviewBroker;
|
|
21
24
|
readonly worker: Worker;
|
|
@@ -8,11 +8,13 @@ import { roleConnection } from "./connection-roles.js";
|
|
|
8
8
|
import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
|
|
9
9
|
import { SessionWorkerRequests } from "./session-worker-requests.js";
|
|
10
10
|
import { acknowledge, acknowledgeGrant } from "./session-worker-signals.js";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
const compiledWorkerEntry = typeof SENPI_RPC_SESSION_WORKER_ENTRY === "string"
|
|
12
13
|
? SENPI_RPC_SESSION_WORKER_ENTRY
|
|
13
14
|
: "./src/modes/rpc/session-worker.ts";
|
|
14
15
|
export class SessionWorkerClient {
|
|
15
16
|
constructor(callbacks) {
|
|
17
|
+
this.privateOperations = new Map();
|
|
16
18
|
/** The worker's own route to the main-thread Bun.WebView service; released when the worker exits. */
|
|
17
19
|
this.webviewBroker = createWebViewBroker();
|
|
18
20
|
this.worker = new Worker(isBunBinary
|
|
@@ -35,6 +37,11 @@ export class SessionWorkerClient {
|
|
|
35
37
|
this.callbacks = callbacks;
|
|
36
38
|
this.exited = new Promise((resolve, reject) => {
|
|
37
39
|
this.worker.once("exit", () => {
|
|
40
|
+
this.stopPrivateBinding?.();
|
|
41
|
+
this.privateBinding?.dispose();
|
|
42
|
+
for (const operation of this.privateOperations.values())
|
|
43
|
+
operation.operation.abort();
|
|
44
|
+
this.privateOperations.clear();
|
|
38
45
|
if (!this.stopped && !this.closeTimer)
|
|
39
46
|
this.fail("session_worker_exited");
|
|
40
47
|
this.stopped = true;
|
|
@@ -111,9 +118,12 @@ export class SessionWorkerClient {
|
|
|
111
118
|
capabilities: options.capabilities ?? [],
|
|
112
119
|
connection: writer.currentConnection(),
|
|
113
120
|
roles: options.connectionRoles?.rolesOf(roleConnection(writer.currentConnection())),
|
|
121
|
+
mcpFormsEnforced: writer.mcpPresenters !== undefined,
|
|
114
122
|
});
|
|
115
123
|
return {
|
|
116
124
|
handle: async (command) => {
|
|
125
|
+
if (writer.handleMcpForm(command))
|
|
126
|
+
return;
|
|
117
127
|
const connection = writer.currentConnection();
|
|
118
128
|
if ("type" in command &&
|
|
119
129
|
command.type === "extension_ui_response" &&
|
|
@@ -177,6 +187,85 @@ export class SessionWorkerClient {
|
|
|
177
187
|
return;
|
|
178
188
|
}
|
|
179
189
|
switch (message.type) {
|
|
190
|
+
case "mcp_form_bind": {
|
|
191
|
+
this.stopPrivateBinding?.();
|
|
192
|
+
this.privateBinding?.dispose();
|
|
193
|
+
const writer = this.writer;
|
|
194
|
+
const sessionId = this.sessionId;
|
|
195
|
+
if (!writer?.mcpPresenters || !sessionId) {
|
|
196
|
+
this.fail("private_form_unavailable");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const bound = writer.bindMcpForms(sessionId, message.durableId, {}, () => { });
|
|
200
|
+
if (bound instanceof Promise) {
|
|
201
|
+
this.fail("private_form_unavailable");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.privateBinding = writer.getMcpFormBinding(sessionId);
|
|
205
|
+
const binding = this.privateBinding;
|
|
206
|
+
if (!binding) {
|
|
207
|
+
this.fail("private_form_unavailable");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const unsubscribe = binding.subscribe?.(() => this.post({
|
|
211
|
+
type: "mcp_form_availability",
|
|
212
|
+
epoch: binding.epoch,
|
|
213
|
+
version: binding.version?.(),
|
|
214
|
+
}));
|
|
215
|
+
this.stopPrivateBinding = () => {
|
|
216
|
+
unsubscribe?.();
|
|
217
|
+
bound?.();
|
|
218
|
+
};
|
|
219
|
+
this.post({ type: "mcp_form_bound", id: message.id, epoch: binding.epoch, version: binding.version?.() });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
case "mcp_form_ready": {
|
|
223
|
+
const binding = this.privateBinding;
|
|
224
|
+
this.post({
|
|
225
|
+
type: "mcp_form_availability",
|
|
226
|
+
id: message.id,
|
|
227
|
+
epoch: message.epoch,
|
|
228
|
+
version: binding?.epoch === message.epoch ? binding.version?.() : undefined,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case "mcp_form_release":
|
|
233
|
+
if (this.privateBinding?.epoch === message.epoch)
|
|
234
|
+
this.stopPrivateBinding?.();
|
|
235
|
+
return;
|
|
236
|
+
case "mcp_form_cancel": {
|
|
237
|
+
const operation = this.privateOperations.get(message.id);
|
|
238
|
+
if (operation?.epoch === message.epoch)
|
|
239
|
+
operation[message.lifetime].abort();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
case "mcp_form_create": {
|
|
243
|
+
const binding = this.privateBinding;
|
|
244
|
+
const presenter = binding?.epoch === message.epoch && binding.version?.() === message.version
|
|
245
|
+
? binding.capture()
|
|
246
|
+
: undefined;
|
|
247
|
+
const operation = new AbortController();
|
|
248
|
+
const presentation = new AbortController();
|
|
249
|
+
this.privateOperations.set(message.id, { operation, presentation, epoch: message.epoch });
|
|
250
|
+
const fence = new Int32Array(message.operationFence);
|
|
251
|
+
const result = presenter?.present({
|
|
252
|
+
...message.input,
|
|
253
|
+
operationSignal: operation.signal,
|
|
254
|
+
presentationSignal: presentation.signal,
|
|
255
|
+
operationCurrent: () => Atomics.load(fence, 0) === 0,
|
|
256
|
+
});
|
|
257
|
+
void Promise.resolve(result).then((outcome) => {
|
|
258
|
+
this.privateOperations.delete(message.id);
|
|
259
|
+
this.post({
|
|
260
|
+
type: "mcp_form_settlement",
|
|
261
|
+
id: message.id,
|
|
262
|
+
epoch: message.epoch,
|
|
263
|
+
settlementId: randomUUID(),
|
|
264
|
+
outcome: outcome ?? { kind: "processing_failure", source: "host_termination", humanAdmission: false },
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
180
269
|
case "prepared":
|
|
181
270
|
case "ready":
|
|
182
271
|
case "result": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BrowserEngine } from "../../core/browser-engine.ts";
|
|
2
|
+
import type { McpFormInput, McpFormOutcome } from "../../core/mcp-form-types.ts";
|
|
2
3
|
import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
3
4
|
import type { MirroredLimits } from "../../core/subscription-limits/limit-store.ts";
|
|
4
5
|
import type { CliRuntimeConfiguration } from "../../main.ts";
|
|
@@ -52,6 +53,23 @@ export type HostToSessionWorker = {
|
|
|
52
53
|
connection?: string;
|
|
53
54
|
/** Roles the connection explicitly declared on the host (senpi#3811); the isolate adopts them. */
|
|
54
55
|
roles?: readonly string[];
|
|
56
|
+
mcpFormsEnforced?: boolean;
|
|
57
|
+
} | {
|
|
58
|
+
type: "mcp_form_bound";
|
|
59
|
+
id: string;
|
|
60
|
+
epoch: string;
|
|
61
|
+
version?: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: "mcp_form_availability";
|
|
64
|
+
epoch: string;
|
|
65
|
+
version?: string;
|
|
66
|
+
id?: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: "mcp_form_settlement";
|
|
69
|
+
id: string;
|
|
70
|
+
epoch: string;
|
|
71
|
+
settlementId: string;
|
|
72
|
+
outcome: McpFormOutcome;
|
|
55
73
|
} | {
|
|
56
74
|
type: "command";
|
|
57
75
|
request: number;
|
|
@@ -84,6 +102,29 @@ export type HostToSessionWorker = {
|
|
|
84
102
|
type: "close";
|
|
85
103
|
};
|
|
86
104
|
export type SessionWorkerToHost = {
|
|
105
|
+
type: "mcp_form_bind";
|
|
106
|
+
id: string;
|
|
107
|
+
durableId: string;
|
|
108
|
+
} | {
|
|
109
|
+
type: "mcp_form_ready";
|
|
110
|
+
id: string;
|
|
111
|
+
epoch: string;
|
|
112
|
+
} | {
|
|
113
|
+
type: "mcp_form_release";
|
|
114
|
+
epoch: string;
|
|
115
|
+
} | {
|
|
116
|
+
type: "mcp_form_cancel";
|
|
117
|
+
id: string;
|
|
118
|
+
epoch: string;
|
|
119
|
+
lifetime: "operation" | "presentation";
|
|
120
|
+
} | {
|
|
121
|
+
type: "mcp_form_create";
|
|
122
|
+
id: string;
|
|
123
|
+
epoch: string;
|
|
124
|
+
version: string;
|
|
125
|
+
operationFence: SharedArrayBuffer;
|
|
126
|
+
input: Omit<McpFormInput, "operationSignal" | "operationCurrent" | "presentationSignal">;
|
|
127
|
+
} | {
|
|
87
128
|
type: "prepared";
|
|
88
129
|
request: number;
|
|
89
130
|
sessionPath: string;
|
|
@@ -23,6 +23,7 @@ import { resolveMovedProfile } from "./session-registry-moved-path.js";
|
|
|
23
23
|
import { createWorkerCredit } from "./session-worker-credit.js";
|
|
24
24
|
import { SESSION_WORKER_LIMITS, } from "./session-worker-protocol.js";
|
|
25
25
|
import { createSessionWorkerRegistry } from "./session-worker-registry.js";
|
|
26
|
+
import { McpFormWorker } from "./mcp-form-worker.js";
|
|
26
27
|
if (isBunBinary) {
|
|
27
28
|
const { registerBunRuntimeModules } = await import("../../bun/runtime-modules.js");
|
|
28
29
|
registerBunRuntimeModules();
|
|
@@ -33,6 +34,8 @@ const port = parentPort;
|
|
|
33
34
|
if (!port)
|
|
34
35
|
throw new Error("Session worker requires a parent port");
|
|
35
36
|
const send = (message) => port.postMessage(message);
|
|
37
|
+
const privateForms = new McpFormWorker(send);
|
|
38
|
+
let privateFormsEnforced = false;
|
|
36
39
|
function failWorker(error) {
|
|
37
40
|
send({ type: "failure", error });
|
|
38
41
|
process.exit(1);
|
|
@@ -41,6 +44,9 @@ const { exchange, installWriteReservation } = createWorkerCredit(send, failWorke
|
|
|
41
44
|
// The host's key for the same file, so a deleted directory neither throws here nor misses (senpi#2285).
|
|
42
45
|
installWriteReservation(canonicalSessionPath);
|
|
43
46
|
class WorkerEventWriter extends SessionEventWriter {
|
|
47
|
+
bindMcpForms(_sessionId, durableId, ui, admit) {
|
|
48
|
+
return privateFormsEnforced ? privateForms.bind(durableId, ui, admit) : undefined;
|
|
49
|
+
}
|
|
44
50
|
constructor() {
|
|
45
51
|
super(() => { });
|
|
46
52
|
this.workerSecurityBridge = true;
|
|
@@ -174,6 +180,7 @@ async function handle(message) {
|
|
|
174
180
|
if (message.roles !== undefined)
|
|
175
181
|
connectionRoles.adopt(roleConnection(message.connection), message.roles);
|
|
176
182
|
const bindingEntry = entry;
|
|
183
|
+
privateFormsEnforced = message.mcpFormsEnforced === true;
|
|
177
184
|
const createBinding = () => createRpcSessionBinding(message.sessionId, bindingEntry, writer, () => send({ type: "request_close" }), {
|
|
178
185
|
capabilities: message.capabilities,
|
|
179
186
|
connectionRoles,
|
|
@@ -262,6 +269,8 @@ async function handle(message) {
|
|
|
262
269
|
// A turn in this isolate reports into its own store; the host mirrors it so its clients see it (senpi#3695).
|
|
263
270
|
subscriptionLimits.subscribeMirror((limits) => send({ type: "subscription_limits", limits }));
|
|
264
271
|
port.on("message", (message) => {
|
|
272
|
+
if (privateForms.receive(message))
|
|
273
|
+
return;
|
|
265
274
|
void handle(message).catch((cause) => {
|
|
266
275
|
const error = cause instanceof Error ? cause.message : String(cause);
|
|
267
276
|
if (error.startsWith("session_worker_")) {
|
package/docs/extensions.md
CHANGED
|
@@ -3507,6 +3507,11 @@ if (confirmed) {
|
|
|
3507
3507
|
|
|
3508
3508
|
#### Manual Dismissal with AbortSignal
|
|
3509
3509
|
|
|
3510
|
+
Local terminal `select`, `confirm`, and `input` dialogs also accept
|
|
3511
|
+
`ephemeral: true`. Their visible contents remain local; they emit no generic
|
|
3512
|
+
blocked events, replacement labels, identifiers, or pending markers. This option is
|
|
3513
|
+
not an RPC field or permission to present MCP input on another session.
|
|
3514
|
+
|
|
3510
3515
|
For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`:
|
|
3511
3516
|
|
|
3512
3517
|
```typescript
|
package/docs/mcp.md
CHANGED
|
@@ -393,9 +393,26 @@ above.
|
|
|
393
393
|
notice.
|
|
394
394
|
- Every server prompt registers as `/mcp:<server>:<prompt>`; invoking it
|
|
395
395
|
collects the prompt's arguments and drops the rendered text into the editor.
|
|
396
|
-
-
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
- Legacy servers may ask form questions mid-call through the existing terminal
|
|
397
|
+
or RPC dialog channel.
|
|
398
|
+
- On MCP 2026-07-28 connections, `tools/call`, `resources/read`, and `prompts/get`
|
|
399
|
+
can return `input_required`. Senpi collects form input privately in the
|
|
400
|
+
originating terminal session, lets you review and edit it, and continues the
|
|
401
|
+
operation with a new request ID. Only the final result reaches the model or editor.
|
|
402
|
+
Continue does not submit a form: even empty forms require Submit. Decline is
|
|
403
|
+
an explicit human refusal; dismissal of a presented form is Cancel.
|
|
404
|
+
- RPC, print, and other runs without an originating terminal presenter do not
|
|
405
|
+
advertise modern elicitation. An unsolicited input request terminates the
|
|
406
|
+
operation locally without fabricating an answer or retrying incomplete input.
|
|
407
|
+
State-only continuations need no human and remain supported.
|
|
408
|
+
- Each operation allows eight input rounds and five minutes of total consent
|
|
409
|
+
time, including queueing. The overall deadline also includes network time
|
|
410
|
+
and cannot be extended by progress. Caller cancellation, session closure,
|
|
411
|
+
or observed authorization invalidation dismisses pending input without a retry.
|
|
412
|
+
Authorization uses the same observed-revocation boundary as ordinary MCP
|
|
413
|
+
requests. Unsupported client features terminate locally.
|
|
414
|
+
- Continuation state and drafts are not persisted or resumed after restart.
|
|
415
|
+
MRTR does not enable the Tasks extension.
|
|
399
416
|
|
|
400
417
|
## Auth
|
|
401
418
|
|