@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
|
@@ -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();
|
|
@@ -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) =>
|
|
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;
|