@code-yeongyu/senpi 2026.10.25-2 → 2026.10.25-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -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-6O4SEW2W.js +5 -0
- package/dist/bundle/chunks/{chunk-Y5OTHMXO.js → chunk-7CNOCJCN.js} +1 -1
- package/dist/bundle/chunks/{chunk-JRXKLIEQ.js → chunk-7W7OJDIY.js} +4 -4
- package/dist/bundle/chunks/chunk-AX72VNRS.js +27 -0
- package/dist/bundle/chunks/{chunk-LYGAV6KU.js → chunk-C5UFJ7B3.js} +1 -1
- package/dist/bundle/chunks/{chunk-G2UETS2R.js → chunk-CNL6QSUB.js} +4 -4
- package/dist/bundle/chunks/{chunk-2EFVQWXZ.js → chunk-FFQOEC76.js} +3 -3
- package/dist/bundle/chunks/chunk-H3HNMY2W.js +13 -0
- package/dist/bundle/chunks/{chunk-W2BKZBYJ.js → chunk-HVJI5T75.js} +1 -1
- package/dist/bundle/chunks/{chunk-YN7ELQ43.js → chunk-JA2R2ZSE.js} +1 -1
- package/dist/bundle/chunks/chunk-JPGT64RV.js +2 -0
- package/dist/bundle/chunks/chunk-KXB5WIS5.js +4 -0
- package/dist/bundle/chunks/chunk-L73UYAH5.js +3 -0
- package/dist/bundle/chunks/chunk-LTU6UFEX.js +2 -0
- package/dist/bundle/chunks/{chunk-KUE4AQN6.js → chunk-MJ5NNWX4.js} +1 -1
- package/dist/bundle/chunks/{chunk-WFXUCVIR.js → chunk-O3Q2ET6W.js} +2 -5
- package/dist/bundle/chunks/{chunk-MXZPAUFB.js → chunk-ODTGC6YU.js} +1 -1
- package/dist/bundle/chunks/chunk-P3M2PF6K.js +27 -0
- package/dist/bundle/chunks/{chunk-VNN2V74Y.js → chunk-SWJXRC2I.js} +1 -1
- package/dist/bundle/chunks/{chunk-RXKNOEQQ.js → chunk-UQNRW3VU.js} +1 -1
- package/dist/bundle/chunks/{chunk-U67ML6CQ.js → chunk-WD6WBYC5.js} +1 -1
- package/dist/bundle/chunks/chunk-WJOT3DFL.js +2 -0
- package/dist/bundle/chunks/{chunk-XBUSVKWC.js → chunk-WLHNEUGD.js} +1 -1
- package/dist/bundle/chunks/chunk-YI7SPFDN.js +5 -0
- package/dist/bundle/chunks/{chunk-J2U5VDG7.js → chunk-YRF4I43T.js} +28 -28
- package/dist/bundle/chunks/chunk-Z3UYNSVR.js +3 -0
- package/dist/bundle/chunks/cli-main-HEKFFB5S.js +3 -0
- package/dist/bundle/chunks/{client-KWTLTKVV.js → client-WTYEICLH.js} +3 -3
- package/dist/bundle/chunks/dispatch-NYAULNVW.js +9 -0
- package/dist/bundle/chunks/{help-fast-path-ZHZGXAUG.js → help-fast-path-3Z26DQSV.js} +1 -1
- package/dist/bundle/chunks/host-command-Z3WPHJOW.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-ONQIMWMX.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +7 -5
- package/dist/bundle/chunks/host-session-command-J65ENSV5.js +20 -0
- package/dist/bundle/chunks/{host-socket-request-AS2OCRLP.js → host-socket-request-3CWE4265.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +2 -0
- package/dist/bundle/chunks/mcp-presenter-bootstrap-XUSCQVRC.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-VTTY5MFT.js → multi-session-host-YI2WZOET.js} +12 -9
- package/dist/bundle/chunks/{package-manager-cli-DZFOC3MF.js → package-manager-cli-GHZPJMRF.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-U7YQMC7H.js → rotation-stream-ZPVCFOVD.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-B5NQ4ZVH.js → rpc-monitor-source-FFEQS744.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-SLISTG4V.js → schedule-command-YWG6D6ZF.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +3 -0
- package/dist/bundle/chunks/{session-picker-465FVXQG.js → session-picker-25H5KNYP.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +278 -264
- 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 +7 -5
- 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 +56 -2
- package/dist/core/extensions/builtin/mcp/expose/call.js +6 -2
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/expose/session.js +7 -0
- package/dist/core/extensions/builtin/mcp/health.js +5 -0
- package/dist/core/extensions/builtin/mcp/host-registry.js +7 -1
- package/dist/core/extensions/builtin/mcp/index.js +2 -0
- package/dist/core/extensions/builtin/mcp/modern/client.d.ts +5 -0
- package/dist/core/extensions/builtin/mcp/modern/client.js +15 -0
- package/dist/core/extensions/builtin/mcp/modern/consent-queue.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +59 -0
- package/dist/core/extensions/builtin/mcp/modern/diagnostics.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/modern/diagnostics.js +39 -0
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +21 -11
- package/dist/core/extensions/builtin/mcp/modern/elicitation.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/modern/elicitation.js +145 -0
- package/dist/core/extensions/builtin/mcp/modern/errors.d.ts +21 -0
- package/dist/core/extensions/builtin/mcp/modern/errors.js +23 -0
- package/dist/core/extensions/builtin/mcp/modern/form-schema.d.ts +20 -0
- package/dist/core/extensions/builtin/mcp/modern/form-schema.js +153 -0
- package/dist/core/extensions/builtin/mcp/modern/http-fetch.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/modern/http-fetch.js +8 -2
- package/dist/core/extensions/builtin/mcp/modern/inputs.d.ts +11 -0
- package/dist/core/extensions/builtin/mcp/modern/inputs.js +65 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +11 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +109 -0
- package/dist/core/extensions/builtin/mcp/modern/request.d.ts +20 -0
- package/dist/core/extensions/builtin/mcp/modern/request.js +292 -0
- package/dist/core/extensions/builtin/mcp/modern/stdio-probe.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +54 -6
- package/dist/core/extensions/builtin/mcp/modern/transport.js +80 -14
- package/dist/core/extensions/builtin/mcp/modern/wire.js +11 -7
- package/dist/core/extensions/builtin/mcp/prompts.d.ts +5 -1
- package/dist/core/extensions/builtin/mcp/prompts.js +6 -1
- package/dist/core/extensions/builtin/mcp/request-origin.d.ts +47 -0
- package/dist/core/extensions/builtin/mcp/request-origin.js +59 -0
- package/dist/core/extensions/builtin/mcp/resources.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/resources.js +19 -4
- package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/sdk.lazy.js +3 -1
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/service-register.js +3 -0
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +2 -1
- package/dist/core/extensions/builtin/mcp/service.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/service.js +79 -4
- package/dist/core/extensions/builtin/mcp/shared-connection.js +36 -2
- package/dist/core/extensions/builtin/mcp/shared-lease.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/shared-lease.js +9 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +3 -2
- package/dist/core/extensions/builtin/mcp/transport-sdk.js +56 -25
- package/dist/core/extensions/builtin/mcp/wrap.d.ts +7 -0
- package/dist/core/extensions/builtin/mcp/wrap.js +6 -0
- package/dist/core/extensions/runner.js +3 -2
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/lifecycle-ledger.d.ts +1 -0
- package/dist/core/lifecycle-ledger.js +7 -2
- package/dist/core/lifecycle-record.js +3 -0
- package/dist/core/lifecycle-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-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/rpc/connection-handler.d.ts +1 -0
- package/dist/modes/rpc/connection-handler.js +7 -1
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-ensure-start.js +13 -2
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +1 -0
- package/dist/modes/rpc/host-handoff.d.ts +2 -0
- package/dist/modes/rpc/host-handover-wire.d.ts +2 -0
- package/dist/modes/rpc/host-handover-wire.js +1 -0
- package/dist/modes/rpc/host-lifecycle-launch.d.ts +1 -0
- package/dist/modes/rpc/host-lifecycle-launch.js +43 -2
- package/dist/modes/rpc/host-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/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 +111 -0
- package/package.json +6 -6
- 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-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/rpc-mode-TOXYD4XA.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-HFFMBALG.js +0 -3
|
@@ -35,6 +35,9 @@ import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/htt
|
|
|
35
35
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
36
36
|
import { isManualContinueSubmission } from "../../core/manual-continue.js";
|
|
37
37
|
import { attachMcpGrantPresentation } from "../../core/mcp-grant-presentations.js";
|
|
38
|
+
import { registerMcpMrtrPresentation } from "../../core/mcp-mrtr-presentations.js";
|
|
39
|
+
import { markMcpFormPresented } from "../../core/mcp-form-types.js";
|
|
40
|
+
import { registerLocalMcpPresenter } from "./mcp-local-presenter.js";
|
|
38
41
|
import { createCompactionSummaryMessage, createCustomMessage } from "../../core/messages.js";
|
|
39
42
|
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope, resolveModelScopeFromModels, resolveModelScopeWithDiagnostics, } from "../../core/model-resolver.js";
|
|
40
43
|
import { CredentialSynchronizationError } from "../../core/model-runtime.js";
|
|
@@ -688,6 +691,7 @@ export class InteractiveMode {
|
|
|
688
691
|
this.onRightClickPaste = () => {
|
|
689
692
|
void this.handleRightClickPaste();
|
|
690
693
|
};
|
|
694
|
+
this.mcpMrtrPresentationLifetime = new AbortController();
|
|
691
695
|
/**
|
|
692
696
|
* Gracefully shutdown the agent.
|
|
693
697
|
* Stops the TUI before emitting shutdown events so extension UI cleanup cannot
|
|
@@ -2010,6 +2014,9 @@ export class InteractiveMode {
|
|
|
2010
2014
|
}
|
|
2011
2015
|
}
|
|
2012
2016
|
async bindCurrentSessionExtensions() {
|
|
2017
|
+
this.mcpMrtrPresentationLifetime?.abort();
|
|
2018
|
+
this.mcpMrtrPresentationLifetime = new AbortController();
|
|
2019
|
+
this.mcpLocalPresenter = undefined;
|
|
2013
2020
|
await this.sessionControlHost?.disposeActive();
|
|
2014
2021
|
this.session.setControlEndpointHost?.(this.sessionControlHost);
|
|
2015
2022
|
const uiContext = this.createExtensionUIContext();
|
|
@@ -2318,7 +2325,12 @@ export class InteractiveMode {
|
|
|
2318
2325
|
return false;
|
|
2319
2326
|
};
|
|
2320
2327
|
}
|
|
2321
|
-
async withBlockedHostDialog(id, label, show) {
|
|
2328
|
+
async withBlockedHostDialog(id, label, show, opts) {
|
|
2329
|
+
if (opts?.ephemeral) {
|
|
2330
|
+
if (opts.signal?.aborted)
|
|
2331
|
+
return undefined;
|
|
2332
|
+
return await show();
|
|
2333
|
+
}
|
|
2322
2334
|
this.session.emitExtensionEvent("herdr:blocked", { active: true, label, id });
|
|
2323
2335
|
try {
|
|
2324
2336
|
return await show();
|
|
@@ -2846,10 +2858,11 @@ export class InteractiveMode {
|
|
|
2846
2858
|
};
|
|
2847
2859
|
}
|
|
2848
2860
|
createExtensionUIContext() {
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2861
|
+
this.mcpMrtrPresentationLifetime ??= new AbortController();
|
|
2862
|
+
const context = {
|
|
2863
|
+
select: (title, options, opts) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionSelector(title, options, opts), opts),
|
|
2864
|
+
confirm: async (title, message, opts) => (await this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionConfirm(title, message, opts), opts)) ?? false,
|
|
2865
|
+
input: (title, placeholder, opts) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionInput(title, placeholder, opts), opts),
|
|
2853
2866
|
// Async answers are delivered by the ask-user extension, not here: the
|
|
2854
2867
|
// widget only resolves the question, so the model sees exactly one
|
|
2855
2868
|
// framed user message no matter which surface answered.
|
|
@@ -2906,6 +2919,11 @@ export class InteractiveMode {
|
|
|
2906
2919
|
getToolsExpanded: () => this.toolOutputExpanded,
|
|
2907
2920
|
setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
|
|
2908
2921
|
};
|
|
2922
|
+
const presenterSession = this.session;
|
|
2923
|
+
const privateForms = this.options?.mcpPresenterEnforcement === true
|
|
2924
|
+
? (this.mcpLocalPresenter ??= registerLocalMcpPresenter(context, presenterSession.sessionId, this.mcpMrtrPresentationLifetime.signal, () => presenterSession.admitHumanAnswer()))
|
|
2925
|
+
: undefined;
|
|
2926
|
+
return registerMcpMrtrPresentation(context, this.mcpMrtrPresentationLifetime.signal, undefined, privateForms);
|
|
2909
2927
|
}
|
|
2910
2928
|
/**
|
|
2911
2929
|
* Show a selector for extensions.
|
|
@@ -2922,7 +2940,8 @@ export class InteractiveMode {
|
|
|
2922
2940
|
};
|
|
2923
2941
|
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2924
2942
|
this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
|
|
2925
|
-
this.
|
|
2943
|
+
if (!opts?.ephemeral || !this.options?.mcpPresenterEnforcement)
|
|
2944
|
+
this.session.admitHumanAnswer();
|
|
2926
2945
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
2927
2946
|
this.hideExtensionSelector();
|
|
2928
2947
|
resolve(option);
|
|
@@ -2939,7 +2958,12 @@ export class InteractiveMode {
|
|
|
2939
2958
|
this.editorContainer.clear();
|
|
2940
2959
|
this.editorContainer.addChild(this.extensionSelector);
|
|
2941
2960
|
this.ui.setFocus(this.extensionSelector);
|
|
2942
|
-
this.
|
|
2961
|
+
if (opts?.ephemeral && this.options?.mcpPresenterEnforcement) {
|
|
2962
|
+
this.ui.renderNow();
|
|
2963
|
+
markMcpFormPresented(opts);
|
|
2964
|
+
}
|
|
2965
|
+
else
|
|
2966
|
+
this.ui.requestRender();
|
|
2943
2967
|
});
|
|
2944
2968
|
}
|
|
2945
2969
|
/**
|
|
@@ -2979,7 +3003,8 @@ export class InteractiveMode {
|
|
|
2979
3003
|
};
|
|
2980
3004
|
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2981
3005
|
this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
|
|
2982
|
-
this.
|
|
3006
|
+
if (!opts?.ephemeral || !this.options?.mcpPresenterEnforcement)
|
|
3007
|
+
this.session.admitHumanAnswer();
|
|
2983
3008
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
2984
3009
|
this.hideExtensionInput();
|
|
2985
3010
|
resolve(value);
|
|
@@ -5505,6 +5530,7 @@ export class InteractiveMode {
|
|
|
5505
5530
|
if (this.isShuttingDown)
|
|
5506
5531
|
return;
|
|
5507
5532
|
this.isShuttingDown = true;
|
|
5533
|
+
this.mcpMrtrPresentationLifetime?.abort();
|
|
5508
5534
|
// Keep signal handlers registered until terminal cleanup has completed.
|
|
5509
5535
|
// `signal-exit` checks the listener list during the same SIGTERM/SIGHUP
|
|
5510
5536
|
// dispatch and re-sends the signal if only its own listeners remain.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "../../core/extensions/types.ts";
|
|
2
|
+
import type { McpFormBinding } from "../../core/mcp-form-types.ts";
|
|
3
|
+
/** The terminal itself is the principal; an RPC attachment never receives this handle. */
|
|
4
|
+
export declare function registerLocalMcpPresenter(ui: ExtensionUIContext, sessionId: string, lifetime: AbortSignal, admit: () => void): McpFormBinding;
|
|
5
|
+
//# sourceMappingURL=mcp-local-presenter.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { collectMrtrForm } from "../../core/extensions/builtin/mcp/modern/elicitation.js";
|
|
2
|
+
import { queuedForm } from "../../core/extensions/builtin/mcp/modern/consent-queue.js";
|
|
3
|
+
import { McpPresenterAuthority } from "../rpc/mcp-presenter-authority.js";
|
|
4
|
+
/** The terminal itself is the principal; an RPC attachment never receives this handle. */
|
|
5
|
+
export function registerLocalMcpPresenter(ui, sessionId, lifetime, admit) {
|
|
6
|
+
const authority = new McpPresenterAuthority();
|
|
7
|
+
const binding = authority.bind(sessionId, sessionId, admit);
|
|
8
|
+
let activeInput;
|
|
9
|
+
const drafts = new Map();
|
|
10
|
+
const sender = authority.connect("local_tui", (frame) => {
|
|
11
|
+
if (frame.type !== "mcp_form_request") {
|
|
12
|
+
drafts.get(frame.dialogId)?.abort();
|
|
13
|
+
drafts.delete(frame.dialogId);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const active = activeInput;
|
|
17
|
+
if (!active)
|
|
18
|
+
return false;
|
|
19
|
+
const { input } = active;
|
|
20
|
+
const draft = new AbortController();
|
|
21
|
+
drafts.set(frame.dialogId, draft);
|
|
22
|
+
const signal = AbortSignal.any([input.operationSignal, input.presentationSignal, lifetime, draft.signal]);
|
|
23
|
+
const { sessionId, bindingEpoch, attachmentEpoch, grantId, dialogId, dialogToken, formRevision } = frame;
|
|
24
|
+
const envelope = { sessionId, bindingEpoch, attachmentEpoch, grantId, dialogId, dialogToken, formRevision };
|
|
25
|
+
void collectMrtrForm({
|
|
26
|
+
ui,
|
|
27
|
+
signal,
|
|
28
|
+
presented: () => authority.receive(sender, { ...envelope, type: "mcp_form_presented" }),
|
|
29
|
+
}, `${frame.server.label} (${frame.server.source})`, { message: frame.message, requestedSchema: frame.schema }, signal)
|
|
30
|
+
.then((result) => {
|
|
31
|
+
if (!signal.aborted)
|
|
32
|
+
authority.receive(sender, { ...envelope, type: "mcp_form_response", ...result });
|
|
33
|
+
}, () => active.fail())
|
|
34
|
+
.finally(() => drafts.delete(frame.dialogId));
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
authority.attach(sender, binding);
|
|
38
|
+
const owner = authority.enroll();
|
|
39
|
+
owner.acquire(owner.register(binding, sender, { kind: "local_tui" }));
|
|
40
|
+
const dispose = () => {
|
|
41
|
+
authority.dispose();
|
|
42
|
+
for (const draft of drafts.values())
|
|
43
|
+
draft.abort();
|
|
44
|
+
drafts.clear();
|
|
45
|
+
};
|
|
46
|
+
lifetime.addEventListener("abort", dispose, { once: true });
|
|
47
|
+
const privateForms = {
|
|
48
|
+
epoch: binding.epoch,
|
|
49
|
+
signal: binding.signal,
|
|
50
|
+
capture: () => {
|
|
51
|
+
const presentation = binding.capture();
|
|
52
|
+
if (!presentation)
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
...presentation,
|
|
56
|
+
present: async (input) => {
|
|
57
|
+
const deadline = performance.now() + input.deadlineMs;
|
|
58
|
+
const timeout = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => timeout.abort(), Math.max(0, input.deadlineMs));
|
|
60
|
+
const signal = AbortSignal.any([
|
|
61
|
+
input.operationSignal,
|
|
62
|
+
input.presentationSignal,
|
|
63
|
+
presentation.signal,
|
|
64
|
+
lifetime,
|
|
65
|
+
timeout.signal,
|
|
66
|
+
]);
|
|
67
|
+
try {
|
|
68
|
+
return await queuedForm(binding, signal, async () => {
|
|
69
|
+
const failure = new AbortController();
|
|
70
|
+
const current = {
|
|
71
|
+
input: {
|
|
72
|
+
...input,
|
|
73
|
+
presentationSignal: AbortSignal.any([signal, failure.signal]),
|
|
74
|
+
deadlineMs: Math.max(0, deadline - performance.now()),
|
|
75
|
+
},
|
|
76
|
+
fail: () => failure.abort(),
|
|
77
|
+
};
|
|
78
|
+
activeInput = current;
|
|
79
|
+
try {
|
|
80
|
+
return await presentation.present(current.input);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (activeInput === current)
|
|
84
|
+
activeInput = undefined;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return {
|
|
90
|
+
kind: input.operationSignal.aborted || binding.signal.aborted || input.operationCurrent?.() === false
|
|
91
|
+
? "operation_aborted"
|
|
92
|
+
: "processing_failure",
|
|
93
|
+
source: "host_termination",
|
|
94
|
+
humanAdmission: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
dispose,
|
|
104
|
+
};
|
|
105
|
+
if (lifetime.aborted)
|
|
106
|
+
dispose();
|
|
107
|
+
return privateForms;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=mcp-local-presenter.js.map
|
|
@@ -34,6 +34,7 @@ export interface RpcConnectionOptions {
|
|
|
34
34
|
hasCapability?: (connectionId: string, capability: string) => boolean;
|
|
35
35
|
};
|
|
36
36
|
bindSecurityPresentation?: (session: AgentSessionRuntime["session"], ui: ExtensionUIContext) => (() => void) | undefined;
|
|
37
|
+
bindMcpForms?: (session: AgentSessionRuntime["session"], ui: ExtensionUIContext) => (() => void) | undefined | Promise<(() => void) | undefined>;
|
|
37
38
|
/** A worker's internal bridge is gated by its host's actual socket capabilities. */
|
|
38
39
|
workerSecurityBridge?: boolean;
|
|
39
40
|
grantDialogSettled?: (id: string) => void;
|
|
@@ -599,6 +599,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
599
599
|
subscribeExtensionEvents();
|
|
600
600
|
};
|
|
601
601
|
const rebindSession = async (deferRefresh = false) => {
|
|
602
|
+
unbindMcpForms?.();
|
|
603
|
+
unbindMcpForms = undefined;
|
|
602
604
|
unsubscribeSecurityNotices?.();
|
|
603
605
|
unsubscribeLoadedSurfaces?.();
|
|
604
606
|
unsubscribeExtensionEvents?.();
|
|
@@ -673,8 +675,10 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
673
675
|
// reconstruct the session it is bound to. Installing here also drops the
|
|
674
676
|
// previous session's subscription immediately instead of after the bind.
|
|
675
677
|
installSessionSubscriptions();
|
|
678
|
+
const extensionUi = createExtensionUIContext();
|
|
679
|
+
unbindMcpForms = await options.bindMcpForms?.(session, extensionUi);
|
|
676
680
|
const refresh = refreshLoadedSurfacesAfter(() => session.bindExtensions({
|
|
677
|
-
uiContext:
|
|
681
|
+
uiContext: extensionUi,
|
|
678
682
|
mode: "rpc",
|
|
679
683
|
commandContextActions: {
|
|
680
684
|
waitForIdle: () => session.agent.waitForIdle(),
|
|
@@ -1752,6 +1756,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1752
1756
|
? subscriptionLimits.subscribe((limits) => eventOutput.enqueueEvent({ type: "subscription_limits", ...limits }))
|
|
1753
1757
|
: () => { };
|
|
1754
1758
|
const dispose = async (disposeOptions = {}) => {
|
|
1759
|
+
unbindMcpForms?.();
|
|
1755
1760
|
grantPresentationAbort.abort();
|
|
1756
1761
|
detachGrantPresentation?.();
|
|
1757
1762
|
unsubscribeSecurityNotices?.();
|
|
@@ -1777,6 +1782,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1777
1782
|
};
|
|
1778
1783
|
// Perform the initial bind synchronously-scheduled so the handler is ready
|
|
1779
1784
|
// as soon as the caller awaits `ready`.
|
|
1785
|
+
let unbindMcpForms;
|
|
1780
1786
|
const ready = rebindSession();
|
|
1781
1787
|
return {
|
|
1782
1788
|
ready,
|
|
@@ -14,5 +14,6 @@ export declare function multiSessionHostCapabilities(options: {
|
|
|
14
14
|
readonly warm: boolean;
|
|
15
15
|
readonly toolRuntime: ToolRuntimeKind;
|
|
16
16
|
readonly negotiated: readonly string[];
|
|
17
|
+
readonly mcpPresenters?: boolean;
|
|
17
18
|
}): string[];
|
|
18
19
|
//# sourceMappingURL=host-capabilities.d.ts.map
|
|
@@ -20,6 +20,7 @@ export function multiSessionHostCapabilities(options) {
|
|
|
20
20
|
return [
|
|
21
21
|
...new Set([
|
|
22
22
|
"multi_session",
|
|
23
|
+
...(options.mcpPresenters ? ["mcp_form_presenter_v1"] : []),
|
|
23
24
|
"session_lifecycle",
|
|
24
25
|
"expected_host_instance",
|
|
25
26
|
"attach_session",
|
|
@@ -80,7 +81,7 @@ export function multiSessionHostCapabilities(options) {
|
|
|
80
81
|
...(options.warm ? [WARM_CAPABILITY] : []),
|
|
81
82
|
...toolRuntimeCapabilities(options.toolRuntime),
|
|
82
83
|
// The host alone says where tool work runs; a negotiated copy would make two answers.
|
|
83
|
-
...options.negotiated.filter((capability) => !isHostOwnedToolRuntimeCapability(capability)),
|
|
84
|
+
...options.negotiated.filter((capability) => capability !== "mcp_form_presenter_v1" && !isHostOwnedToolRuntimeCapability(capability)),
|
|
84
85
|
]),
|
|
85
86
|
];
|
|
86
87
|
}
|
|
@@ -17,6 +17,7 @@ import { pollProtocolInfo } from "./host-readiness.js";
|
|
|
17
17
|
import { initialHostEnvironment } from "./host-spawn-environment.js";
|
|
18
18
|
import { hostLaunchProfile } from "./protocol-identity.js";
|
|
19
19
|
import { createSocketSecret, socketSecretPath } from "./socket-transport.js";
|
|
20
|
+
import { relayPresenterBootstrap } from "./mcp-presenter-bootstrap.js";
|
|
20
21
|
export const DEFAULT_READINESS_TIMEOUT_MS = 10_000;
|
|
21
22
|
export { DEFAULT_STOP_TIMEOUT_MS };
|
|
22
23
|
/**
|
|
@@ -55,11 +56,13 @@ export async function startHost(paths, socket, options, generation = 0) {
|
|
|
55
56
|
let child;
|
|
56
57
|
let exitedEarly;
|
|
57
58
|
let childExit;
|
|
59
|
+
let presenterEnrollment;
|
|
58
60
|
try {
|
|
59
61
|
const supervisorArgs = [
|
|
60
62
|
"--socket",
|
|
61
63
|
socket,
|
|
62
64
|
...(options.owner ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
|
|
65
|
+
...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
|
|
63
66
|
...(options.hostArgs ?? []),
|
|
64
67
|
];
|
|
65
68
|
const launch = testOptions?.spawn ?? testOptions?.launch?.(supervisorArgs) ?? defaultHostLaunch(supervisorArgs);
|
|
@@ -73,8 +76,14 @@ export async function startHost(paths, socket, options, generation = 0) {
|
|
|
73
76
|
instanceId,
|
|
74
77
|
generation,
|
|
75
78
|
}),
|
|
76
|
-
stdio: options.
|
|
79
|
+
stdio: options.presenterBootstrap
|
|
80
|
+
? ["ignore", "ignore", stderr.fd, options.owner ? "pipe" : "ignore", "pipe"]
|
|
81
|
+
: options.owner
|
|
82
|
+
? ["ignore", "ignore", stderr.fd, "pipe"]
|
|
83
|
+
: ["ignore", "ignore", stderr.fd],
|
|
77
84
|
});
|
|
85
|
+
if (options.presenterBootstrap)
|
|
86
|
+
presenterEnrollment = relayPresenterBootstrap(options.presenterBootstrap, child);
|
|
78
87
|
childExit = new Promise((resolveExit) => {
|
|
79
88
|
child.once("exit", (code, signal) => {
|
|
80
89
|
exitedEarly = { code, signal };
|
|
@@ -140,8 +149,10 @@ export async function startHost(paths, socket, options, generation = 0) {
|
|
|
140
149
|
}
|
|
141
150
|
const readinessTimeoutMs = testOptions?.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
|
|
142
151
|
const result = await pollProtocolInfo(socket, readinessTimeoutMs, isCompatible, childExit);
|
|
143
|
-
if (result.ready)
|
|
152
|
+
if (result.ready) {
|
|
153
|
+
await presenterEnrollment;
|
|
144
154
|
return { pid: pidFile.pid, socket, reused: false, release: result.hold.release };
|
|
155
|
+
}
|
|
145
156
|
await testOptions?.beforeReadinessTeardown?.();
|
|
146
157
|
// Teardown runs for the diagnostic's sake, so it must never replace it: a stop failure here
|
|
147
158
|
// (unreadable identity, a host that outlives SIGKILL) would otherwise propagate instead of the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The options and result of `ensureHost`, kept apart so the ensure's helpers can name them without a cycle. */
|
|
2
2
|
import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
3
4
|
/**
|
|
4
5
|
* What an ensure may do to a host that is already running.
|
|
5
6
|
*
|
|
@@ -10,6 +11,7 @@ import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
|
10
11
|
*/
|
|
11
12
|
export type HostUpgradePolicy = "never" | "if-engine-differs";
|
|
12
13
|
export interface EnsureHostOptions {
|
|
14
|
+
readonly presenterBootstrap?: Duplex;
|
|
13
15
|
readonly socket: string;
|
|
14
16
|
readonly agentDir?: string;
|
|
15
17
|
readonly keyRoot?: string;
|
|
@@ -205,6 +205,7 @@ async function upgradeGeneration(paths, socket, options, attachedPid) {
|
|
|
205
205
|
socket,
|
|
206
206
|
agentDir: options.agentDir ?? getAgentDir(),
|
|
207
207
|
hostArgs: options.hostArgs ?? [],
|
|
208
|
+
presenterBootstrap: options.presenterBootstrap,
|
|
208
209
|
...(options.env ? { env: options.env } : {}),
|
|
209
210
|
...(options.policy ? { policy: options.policy } : {}),
|
|
210
211
|
_test: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Duplex } from "node:stream";
|
|
1
2
|
import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
2
3
|
/**
|
|
3
4
|
* The longest a handoff holds the ensure lock: its probe of the running host, the two session counts
|
|
@@ -5,6 +6,7 @@ import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
|
5
6
|
*/
|
|
6
7
|
export declare const HANDOFF_LOCK_HOLD_MS: number;
|
|
7
8
|
export interface HandoffHostOptions {
|
|
9
|
+
readonly presenterBootstrap?: Duplex;
|
|
8
10
|
readonly socket: string;
|
|
9
11
|
readonly agentDir?: string;
|
|
10
12
|
/** Extra CLI arguments the successor's host child is launched with (provider pinning, extensions). */
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { HandoverAnswer, HandoverOutcome, HandoverView, IdleHandoverRequest } from "./host-idle-handover.ts";
|
|
10
10
|
import type { RpcResponse } from "./rpc-types.ts";
|
|
11
11
|
import type { RpcSessionRegistry } from "./session-registry.ts";
|
|
12
|
+
import type { Duplex } from "node:stream";
|
|
12
13
|
export declare const BEGIN_HANDOVER_COMMAND = "begin_handover";
|
|
13
14
|
export declare function parseBeginHandover(record: Readonly<Record<string, unknown>>): IdleHandoverRequest | string;
|
|
14
15
|
export declare function beginHandoverResponse(id: unknown, answer: HandoverAnswer | string): Record<string, unknown>;
|
|
@@ -37,5 +38,6 @@ export declare function performIdleHandover(context: {
|
|
|
37
38
|
readonly request: IdleHandoverRequest;
|
|
38
39
|
readonly socket: string;
|
|
39
40
|
readonly agentDir: string;
|
|
41
|
+
readonly presenterBootstrap?: Duplex;
|
|
40
42
|
}): Promise<HandoverOutcome>;
|
|
41
43
|
//# sourceMappingURL=host-handover-wire.d.ts.map
|
|
@@ -120,6 +120,7 @@ export async function performIdleHandover(context) {
|
|
|
120
120
|
env[name] = null;
|
|
121
121
|
Object.assign(env, request.env);
|
|
122
122
|
const result = await handoffHost({
|
|
123
|
+
presenterBootstrap: context.presenterBootstrap,
|
|
123
124
|
socket,
|
|
124
125
|
agentDir,
|
|
125
126
|
hostArgs: request.hostArgs,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { type ChildProcess } from "node:child_process";
|
|
7
7
|
import type { SocketFileIdentity } from "./socket-ownership.ts";
|
|
8
8
|
export interface SupervisorLaunch {
|
|
9
|
+
readonly presenterBootstrapFd?: number;
|
|
9
10
|
readonly socket: string;
|
|
10
11
|
readonly hostArgs: readonly string[];
|
|
11
12
|
/** Optional runtime command used by rebranded/bundled callers. */
|
|
@@ -56,10 +56,16 @@ export function parseSupervisorArgs(argv) {
|
|
|
56
56
|
let childArgs;
|
|
57
57
|
let agentDir;
|
|
58
58
|
let ownerFd;
|
|
59
|
+
let presenterBootstrapFd;
|
|
59
60
|
let bindSocket;
|
|
60
61
|
let replaceIdentity;
|
|
61
62
|
for (let index = 0; index < argv.length; index++) {
|
|
62
63
|
const arg = argv[index];
|
|
64
|
+
if (arg === "--mcp-presenter-fd" && argv[index + 1] === "4") {
|
|
65
|
+
presenterBootstrapFd = 4;
|
|
66
|
+
index++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
63
69
|
if (arg === "--owner-fd" && index + 1 < argv.length) {
|
|
64
70
|
ownerFd = Number(argv[++index]);
|
|
65
71
|
if (!Number.isSafeInteger(ownerFd) || ownerFd < 3)
|
|
@@ -101,7 +107,19 @@ export function parseSupervisorArgs(argv) {
|
|
|
101
107
|
}
|
|
102
108
|
return socket === undefined
|
|
103
109
|
? undefined
|
|
104
|
-
:
|
|
110
|
+
: ownerFd === presenterBootstrapFd && ownerFd !== undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: {
|
|
113
|
+
socket,
|
|
114
|
+
hostArgs,
|
|
115
|
+
childCommand,
|
|
116
|
+
childArgs,
|
|
117
|
+
agentDir,
|
|
118
|
+
ownerFd,
|
|
119
|
+
bindSocket,
|
|
120
|
+
replaceIdentity,
|
|
121
|
+
presenterBootstrapFd,
|
|
122
|
+
};
|
|
105
123
|
}
|
|
106
124
|
export const OWNER_WATCH_FD = 3;
|
|
107
125
|
// Strongly retain the write end until that supervisor exits, but never keep the caller alive.
|
|
@@ -188,6 +206,10 @@ export const HOST_CHILD_WATCH_FD = 3;
|
|
|
188
206
|
export function spawnHostChild(options) {
|
|
189
207
|
const { launch, internalSocket, internal, internalSecret, publicSocket } = options;
|
|
190
208
|
const childLaunch = spawnableChildLaunch(resolveHostChildLaunch(launch, internalSocket));
|
|
209
|
+
// A shell wrapper cannot prove preservation of the private inherited handle.
|
|
210
|
+
const bootstrap = launch.presenterBootstrapFd !== undefined && !childLaunch.shell;
|
|
211
|
+
if (bootstrap)
|
|
212
|
+
childLaunch.args.push("--mcp-presenter-fd", "4");
|
|
191
213
|
const child = spawn(childLaunch.command, childLaunch.args, {
|
|
192
214
|
env: {
|
|
193
215
|
...process.env,
|
|
@@ -210,13 +232,32 @@ export function spawnHostChild(options) {
|
|
|
210
232
|
// Slot 3 is the lifetime pipe: "pipe" gives the child a read end it can
|
|
211
233
|
// wait on and keeps the write end owned by this process alone.
|
|
212
234
|
shell: childLaunch.shell,
|
|
213
|
-
stdio: [
|
|
235
|
+
stdio: [
|
|
236
|
+
process.platform === "win32" ? "ignore" : "pipe",
|
|
237
|
+
"ignore",
|
|
238
|
+
"inherit",
|
|
239
|
+
"pipe",
|
|
240
|
+
...(bootstrap ? ["pipe"] : []),
|
|
241
|
+
],
|
|
214
242
|
// The supervisor is spawned detached, so on win32 it owns no console. A
|
|
215
243
|
// console-subsystem child started from it would allocate a fresh one,
|
|
216
244
|
// which Windows Terminal renders as an empty window that takes focus.
|
|
217
245
|
// CREATE_NO_WINDOW gives the child a console with no window instead.
|
|
218
246
|
windowsHide: true,
|
|
219
247
|
});
|
|
248
|
+
if (launch.presenterBootstrapFd !== undefined) {
|
|
249
|
+
const fd = launch.presenterBootstrapFd;
|
|
250
|
+
// The opt-in helper stays outside the supervisor's fixed startup import budget.
|
|
251
|
+
void import("./mcp-presenter-bootstrap.js")
|
|
252
|
+
.then(({ presenterBootstrapStream, relayPresenterBootstrap }) => {
|
|
253
|
+
const upstream = presenterBootstrapStream(fd);
|
|
254
|
+
if (upstream && bootstrap)
|
|
255
|
+
return relayPresenterBootstrap(upstream, child);
|
|
256
|
+
upstream?.destroy();
|
|
257
|
+
child.stdio[4]?.destroy();
|
|
258
|
+
})
|
|
259
|
+
.catch(() => child.stdio[4]?.destroy());
|
|
260
|
+
}
|
|
220
261
|
if (options.internalKey) {
|
|
221
262
|
child.stdin?.on("error", () => { });
|
|
222
263
|
// One write only. Keep its write end alive until this supervisor exits.
|
|
@@ -5,6 +5,7 @@ import type { ResolvedHostLaunchSpec } from "./host-launch-spec.ts";
|
|
|
5
5
|
import { type HostOutcome } from "./host-outcome.ts";
|
|
6
6
|
import type { HostStopGeneration } from "./host-stop-generation.ts";
|
|
7
7
|
export { HOST_EXIT_ERROR, HOST_EXIT_FALLBACK, HOST_EXIT_OK, HOST_EXIT_REFUSED, HOST_EXIT_USAGE, type HostOutcome, } from "./host-outcome.ts";
|
|
8
|
+
import type { Duplex } from "node:stream";
|
|
8
9
|
/** Which daemon a request is about: the endpoint, and the agent directory holding its state. */
|
|
9
10
|
export interface HostTarget {
|
|
10
11
|
readonly socket: string;
|
|
@@ -50,5 +51,6 @@ export type HostRequest = {
|
|
|
50
51
|
};
|
|
51
52
|
export declare function runHostRequest(request: HostRequest, options?: {
|
|
52
53
|
readonly keyRoot?: string;
|
|
54
|
+
readonly presenterBootstrap?: Duplex;
|
|
53
55
|
}): Promise<HostOutcome>;
|
|
54
56
|
//# sourceMappingURL=host-runner.d.ts.map
|
|
@@ -37,9 +37,9 @@ import { hostRequestKeyRoot } from "./host-request-key-root.js";
|
|
|
37
37
|
import { withRpcKeyRoot } from "./rpc-auth-client.js";
|
|
38
38
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
39
39
|
export async function runHostRequest(request, options = {}) {
|
|
40
|
-
return withRpcKeyRoot(hostRequestKeyRoot(request, options.keyRoot), () => runHostRequestInKeyRoot(request));
|
|
40
|
+
return withRpcKeyRoot(hostRequestKeyRoot(request, options.keyRoot), () => runHostRequestInKeyRoot(request, options.presenterBootstrap));
|
|
41
41
|
}
|
|
42
|
-
async function runHostRequestInKeyRoot(request) {
|
|
42
|
+
async function runHostRequestInKeyRoot(request, presenterBootstrap) {
|
|
43
43
|
if (request.action === "ensure" || request.action === "handoff" || request.action === "stop") {
|
|
44
44
|
const { socket, agentDir } = request.target;
|
|
45
45
|
if ((await endpointKindOfSocket(socket, agentDir)) === "tui") {
|
|
@@ -48,7 +48,7 @@ async function runHostRequestInKeyRoot(request) {
|
|
|
48
48
|
}
|
|
49
49
|
switch (request.action) {
|
|
50
50
|
case "ensure":
|
|
51
|
-
return ensureOutcome(request.target, request.spec, request.policy);
|
|
51
|
+
return ensureOutcome(request.target, request.spec, request.policy, presenterBootstrap);
|
|
52
52
|
case "status":
|
|
53
53
|
return request.all === true
|
|
54
54
|
? statusAllOutcome(request.target, request.includeWorkers)
|
|
@@ -57,7 +57,7 @@ async function runHostRequestInKeyRoot(request) {
|
|
|
57
57
|
return stopOutcome(request);
|
|
58
58
|
case "handoff":
|
|
59
59
|
return request.handover === undefined
|
|
60
|
-
? handoffOutcome(request.target, request.spec)
|
|
60
|
+
? handoffOutcome(request.target, request.spec, presenterBootstrap)
|
|
61
61
|
: idleHandoverOutcome(request.target, request.spec, request.handover);
|
|
62
62
|
case "shard_path":
|
|
63
63
|
return shardPathOutcome(request.kind, request.owner, request.root);
|
|
@@ -70,7 +70,7 @@ async function runHostRequestInKeyRoot(request) {
|
|
|
70
70
|
return assertNever(request);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
async function ensureOutcome(target, spec, policy) {
|
|
73
|
+
async function ensureOutcome(target, spec, policy, presenterBootstrap) {
|
|
74
74
|
const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
|
|
75
75
|
if (policy === "fallback") {
|
|
76
76
|
// `fallback` is the caller saying it can live without a host. The decision is made HERE
|
|
@@ -84,6 +84,7 @@ async function ensureOutcome(target, spec, policy) {
|
|
|
84
84
|
try {
|
|
85
85
|
ensured = await ensureHost({
|
|
86
86
|
socket: target.socket,
|
|
87
|
+
presenterBootstrap,
|
|
87
88
|
agentDir: target.agentDir,
|
|
88
89
|
hostArgs: spec.hostArgs,
|
|
89
90
|
env: daemonEnvOverrides(process.env, spec.env),
|
|
@@ -181,10 +182,11 @@ async function stopOutcome(request) {
|
|
|
181
182
|
},
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
|
-
async function handoffOutcome(target, spec) {
|
|
185
|
+
async function handoffOutcome(target, spec, presenterBootstrap) {
|
|
185
186
|
const before = await probeProtocolInfo(target.socket, PROBE_TIMEOUT_MS);
|
|
186
187
|
const result = await handoffHost({
|
|
187
188
|
socket: target.socket,
|
|
189
|
+
presenterBootstrap,
|
|
188
190
|
agentDir: target.agentDir,
|
|
189
191
|
hostArgs: spec.hostArgs,
|
|
190
192
|
env: daemonEnvOverrides(process.env, spec.env),
|
|
@@ -28,6 +28,7 @@ import { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from "./host-launc
|
|
|
28
28
|
import { DEFAULT_HOST_IDLE_EXIT_MS } from "./host-lifecycle.js";
|
|
29
29
|
import { keepOwnerPipe, OWNER_WATCH_FD } from "./host-lifecycle-launch.js";
|
|
30
30
|
import { probeProtocolInfo } from "./host-probe.js";
|
|
31
|
+
import { relayPresenterBootstrap } from "./mcp-presenter-bootstrap.js";
|
|
31
32
|
import { successorHostEnvironment } from "./host-spawn-environment.js";
|
|
32
33
|
import { signalGeneration } from "./host-stop.js";
|
|
33
34
|
import { hostLaunchProfile } from "./protocol-identity.js";
|
|
@@ -81,6 +82,7 @@ export async function startSuccessor(context) {
|
|
|
81
82
|
"--replace",
|
|
82
83
|
`${replaced.dev}:${replaced.ino}`,
|
|
83
84
|
...(inheritOwnerPipe ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
|
|
85
|
+
...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
|
|
84
86
|
...(options.hostArgs ?? []),
|
|
85
87
|
];
|
|
86
88
|
const launch = (options._test?.launch ?? options.launch)?.(argv) ?? defaultHostLaunch(argv);
|
|
@@ -96,8 +98,15 @@ export async function startSuccessor(context) {
|
|
|
96
98
|
generation,
|
|
97
99
|
instanceId,
|
|
98
100
|
}),
|
|
99
|
-
stdio:
|
|
101
|
+
stdio: options.presenterBootstrap
|
|
102
|
+
? ["ignore", "ignore", stderr.fd, inheritOwnerPipe ? "pipe" : "ignore", "pipe"]
|
|
103
|
+
: inheritOwnerPipe
|
|
104
|
+
? ["ignore", "ignore", stderr.fd, "pipe"]
|
|
105
|
+
: ["ignore", "ignore", stderr.fd],
|
|
100
106
|
});
|
|
107
|
+
const presenterEnrollment = options.presenterBootstrap
|
|
108
|
+
? relayPresenterBootstrap(options.presenterBootstrap, spawned)
|
|
109
|
+
: undefined;
|
|
101
110
|
child = spawned;
|
|
102
111
|
exited = new Promise((resolve) => spawned.once("exit", () => resolve()));
|
|
103
112
|
await stderr.close();
|
|
@@ -135,6 +144,7 @@ export async function startSuccessor(context) {
|
|
|
135
144
|
// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.
|
|
136
145
|
await options._test?.beforeRegistration?.();
|
|
137
146
|
await writeHostRegistration(paths, { ...registration, ...successorBuild(answer) });
|
|
147
|
+
await presenterEnrollment;
|
|
138
148
|
child.unref();
|
|
139
149
|
// The successor owns the socket now: the predecessor may drain. SIGUSR1 is sent only here,
|
|
140
150
|
// to a pid the record proved and a host that advertised it can survive the signal. A
|