@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
|
@@ -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
|
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The terminal itself is one attachment; each attached client adds one. A client that only subscribed
|
|
9
9
|
* (mirror included) is not an attachment.
|
|
10
|
+
*
|
|
11
|
+
* It also keeps what a host keeps per connection and per session for the drive set (senpi#4267): the roles each
|
|
12
|
+
* connection declared with `set_client_info`, and the admission receipt of every input sent with a `clientMessageId`,
|
|
13
|
+
* so a retry is answered from its receipt instead of being delivered twice.
|
|
10
14
|
*/
|
|
11
15
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
16
|
+
import { ConnectionRoles } from "../rpc/connection-roles.ts";
|
|
12
17
|
import { SessionEventWriter } from "../rpc/session-event-writer.ts";
|
|
13
18
|
import type { ControlConnection } from "./session-control-server.ts";
|
|
14
19
|
export declare class ControlAttachments {
|
|
15
20
|
readonly writer: SessionEventWriter;
|
|
21
|
+
readonly roles: ConnectionRoles;
|
|
22
|
+
readonly receipts: Map<string, TerminalInputReceipt>;
|
|
16
23
|
private readonly attached;
|
|
17
24
|
private readonly stopRecords;
|
|
18
25
|
private readonly session;
|
|
@@ -21,6 +28,8 @@ export declare class ControlAttachments {
|
|
|
21
28
|
get handle(): string;
|
|
22
29
|
opened(connection: ControlConnection): void;
|
|
23
30
|
closed(connection: ControlConnection): void;
|
|
31
|
+
/** Drops this connection's attachment (`release_attachment`); false when it was not attached. */
|
|
32
|
+
detach(connection: ControlConnection): boolean;
|
|
24
33
|
/** Attaches the connection: it is replayed the in-flight snapshot, then receives the session's records. */
|
|
25
34
|
attach(connection: ControlConnection): boolean;
|
|
26
35
|
isAttached(connection: ControlConnection): boolean;
|
|
@@ -30,6 +39,17 @@ export declare class ControlAttachments {
|
|
|
30
39
|
dispose(): void;
|
|
31
40
|
private entryView;
|
|
32
41
|
}
|
|
42
|
+
/** The receipt a terminal answers an input sent with a `clientMessageId` with, and answers again on a retry. */
|
|
43
|
+
export interface TerminalInputReceipt {
|
|
44
|
+
/** What was sent: a retry with the same id and different content is a conflict, as on a host. */
|
|
45
|
+
readonly fingerprint: string;
|
|
46
|
+
readonly disposition: "started" | "queued";
|
|
47
|
+
readonly admission: {
|
|
48
|
+
readonly clientMessageId: string;
|
|
49
|
+
readonly durableSessionId: string;
|
|
50
|
+
readonly disposition: "started" | "queued";
|
|
51
|
+
};
|
|
52
|
+
}
|
|
33
53
|
/** The writer's id for one control connection. */
|
|
34
54
|
export declare function keyOf(connection: ControlConnection): string;
|
|
35
55
|
//# sourceMappingURL=session-control-attach.d.ts.map
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { ConnectionRoles } from "../rpc/connection-roles.js";
|
|
1
2
|
import { SessionEventWriter } from "../rpc/session-event-writer.js";
|
|
2
3
|
import { subscribeSessionRecords } from "../rpc/session-record-stream.js";
|
|
3
4
|
import { sessionRuntimeView } from "../rpc/session-status-source.js";
|
|
4
5
|
export class ControlAttachments {
|
|
5
6
|
constructor(session, pendingQuestions) {
|
|
6
7
|
this.writer = new SessionEventWriter(() => { });
|
|
8
|
+
this.roles = new ConnectionRoles();
|
|
9
|
+
this.receipts = new Map();
|
|
7
10
|
this.attached = new Set();
|
|
8
11
|
this.session = session;
|
|
9
12
|
this.writer.status.setSource({
|
|
@@ -29,8 +32,18 @@ export class ControlAttachments {
|
|
|
29
32
|
const key = keyOf(connection);
|
|
30
33
|
if (this.attached.delete(key))
|
|
31
34
|
this.writer.status.publish(this.handle, "detach", "client");
|
|
35
|
+
this.roles.release(key);
|
|
32
36
|
this.writer.unregisterConnection(key);
|
|
33
37
|
}
|
|
38
|
+
/** Drops this connection's attachment (`release_attachment`); false when it was not attached. */
|
|
39
|
+
detach(connection) {
|
|
40
|
+
const key = keyOf(connection);
|
|
41
|
+
if (!this.attached.delete(key))
|
|
42
|
+
return false;
|
|
43
|
+
this.writer.detachConnectionFromSession(key, this.handle);
|
|
44
|
+
this.writer.status.publish(this.handle, "detach", "client");
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
34
47
|
/** Attaches the connection: it is replayed the in-flight snapshot, then receives the session's records. */
|
|
35
48
|
attach(connection) {
|
|
36
49
|
const key = keyOf(connection);
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* The terminal control endpoint's command surface. It answers who it is, which one session it holds
|
|
3
3
|
* and that session's state and messages, renames it, streams its feed, wakes its inbox drain, relays
|
|
4
4
|
* an answer to a question the session itself asked, and applies the pane's own session controls (a
|
|
5
|
-
* model, a thinking level, an interrupt - `session-control-session-commands.ts`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* model, a thinking level, an interrupt - `session-control-session-commands.ts`), and serves the host's
|
|
6
|
+
* observe-and-drive set over its one session (`session-control-rpc-commands.ts`, senpi#3229, senpi#4267). Input
|
|
7
|
+
* goes through the session's admission and only there. Every other command is answered `unsupported` as data;
|
|
8
|
+
* `get_protocol_info` lists the accepted ones in `commands`.
|
|
9
9
|
*/
|
|
10
10
|
import { VERSION } from "../../config.js";
|
|
11
11
|
import { engineBuildIdentity } from "../../core/engine-build-identity.js";
|
|
12
|
-
import { SESSION_STATUS_CAPABILITY } from "../rpc/custom-capability.js";
|
|
12
|
+
import { ENTRIES_TAIL_CAPABILITY, OBSERVE_READS_CAPABILITY, SESSION_STATUS_CAPABILITY, TUI_OBSERVER_CAPABILITY, } from "../rpc/custom-capability.js";
|
|
13
|
+
import { answerGetEntries } from "../rpc/get-entries-command.js";
|
|
13
14
|
import { answeredUiRequestId, settledQuestionStatus, unansweredQuestionIds } from "../rpc/extension-ui-response.js";
|
|
14
15
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
15
16
|
import { runTuiRpcCommand, TUI_RPC_COMMANDS } from "./session-control-rpc-commands.js";
|
|
@@ -21,6 +22,7 @@ const TUI_CONTROL_COMMANDS = [
|
|
|
21
22
|
"list_sessions",
|
|
22
23
|
"get_state",
|
|
23
24
|
"get_messages",
|
|
25
|
+
"get_entries",
|
|
24
26
|
"set_session_name",
|
|
25
27
|
"subscribe",
|
|
26
28
|
"wake",
|
|
@@ -37,7 +39,13 @@ export async function runControlCommand(context, connection, command) {
|
|
|
37
39
|
return success(id, type, {
|
|
38
40
|
protocolVersion: 1,
|
|
39
41
|
serverVersion: VERSION,
|
|
40
|
-
capabilities: [
|
|
42
|
+
capabilities: [
|
|
43
|
+
TUI_CONTROL_CAPABILITY,
|
|
44
|
+
SESSION_STATUS_CAPABILITY,
|
|
45
|
+
TUI_OBSERVER_CAPABILITY,
|
|
46
|
+
OBSERVE_READS_CAPABILITY,
|
|
47
|
+
ENTRIES_TAIL_CAPABILITY,
|
|
48
|
+
],
|
|
41
49
|
mode: "tui",
|
|
42
50
|
endpoint_kind: "tui",
|
|
43
51
|
instanceId: context.instanceId,
|
|
@@ -60,6 +68,8 @@ export async function runControlCommand(context, connection, command) {
|
|
|
60
68
|
});
|
|
61
69
|
case "get_messages":
|
|
62
70
|
return success(id, type, { messages: session.messages });
|
|
71
|
+
case "get_entries":
|
|
72
|
+
return answerGetEntries(session, command);
|
|
63
73
|
case "set_session_name": {
|
|
64
74
|
const name = typeof command.name === "string" ? command.name.trim() : "";
|
|
65
75
|
if (name === "")
|
|
@@ -3,7 +3,8 @@ import { type HostDaemonPaths } from "../rpc/host-daemon-paths.ts";
|
|
|
3
3
|
* `<agentDir>/rpc/tui/t-<16hex>.sock`, or - when the agent directory is too deep for `sun_path` -
|
|
4
4
|
* the same name under `/tmp/senpi-rpc-<sha256(agentDir)[:8]>/tui/`. The fallback root is
|
|
5
5
|
* predictable, so it is used only when it is a real directory owned by this user with no group or
|
|
6
|
-
* other access; anything else fails the registration instead of binding into it.
|
|
6
|
+
* other access; anything else fails the registration instead of binding into it. On win32 the path is only the
|
|
7
|
+
* logical address (the listener binds a named pipe hashed from it), so it never moves (senpi#4267).
|
|
7
8
|
*/
|
|
8
9
|
export declare function resolveTuiSocket(agentDir: string, instanceId: string): Promise<string>;
|
|
9
10
|
export interface TuiRegistryEntry {
|
|
@@ -27,12 +27,13 @@ const REGISTRY_LOCK_WAIT_MS = 5_000;
|
|
|
27
27
|
* `<agentDir>/rpc/tui/t-<16hex>.sock`, or - when the agent directory is too deep for `sun_path` -
|
|
28
28
|
* the same name under `/tmp/senpi-rpc-<sha256(agentDir)[:8]>/tui/`. The fallback root is
|
|
29
29
|
* predictable, so it is used only when it is a real directory owned by this user with no group or
|
|
30
|
-
* other access; anything else fails the registration instead of binding into it.
|
|
30
|
+
* other access; anything else fails the registration instead of binding into it. On win32 the path is only the
|
|
31
|
+
* logical address (the listener binds a named pipe hashed from it), so it never moves (senpi#4267).
|
|
31
32
|
*/
|
|
32
33
|
export async function resolveTuiSocket(agentDir, instanceId) {
|
|
33
34
|
const name = tuiSocketName(instanceId);
|
|
34
35
|
const primary = join(agentDir, "rpc", "tui", name);
|
|
35
|
-
if (Buffer.byteLength(primary) <= MAX_SOCKET_PATH_BYTES) {
|
|
36
|
+
if (process.platform === "win32" || Buffer.byteLength(primary) <= MAX_SOCKET_PATH_BYTES) {
|
|
36
37
|
await makePrivateDirectory(dirname(primary));
|
|
37
38
|
return primary;
|
|
38
39
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
2
|
import { type ControlAttachments } from "./session-control-attach.ts";
|
|
3
|
-
import type
|
|
4
|
-
export declare const TUI_RPC_COMMANDS: readonly ["open_session", "subscribe_sessions", "unsubscribe_sessions", "steer", "follow_up", "prompt", "get_effect_status"];
|
|
3
|
+
import { type ControlCommand, type ControlConnection } from "./session-control-server.ts";
|
|
4
|
+
export declare const TUI_RPC_COMMANDS: readonly ["open_session", "subscribe_sessions", "unsubscribe_sessions", "steer", "follow_up", "prompt", "get_effect_status", "set_client_info", "attach_session", "release_attachment", "abort"];
|
|
5
5
|
export interface RpcCommandContext {
|
|
6
|
+
/** This endpoint's instance id: the `expectedHostInstance` an `attach_session` may name. */
|
|
7
|
+
readonly instanceId: string;
|
|
6
8
|
readonly session: AgentSession;
|
|
7
9
|
readonly attachments: ControlAttachments;
|
|
8
10
|
}
|
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
* attaches by path or id, `subscribe_sessions`/`unsubscribe_sessions` run the host's status stream over
|
|
4
4
|
* this one session, `steer`/`follow_up`/`prompt` go through the session's external admission (the
|
|
5
5
|
* terminal's one delivery authority, so the user's draft still holds them) with the host's `whenIdle`
|
|
6
|
-
* meaning, and `get_effect_status` reads the same effect ledger.
|
|
6
|
+
* meaning, and `get_effect_status` reads the same effect ledger. The rest of the host's drive set answers with the
|
|
7
|
+
* host's contract (senpi#4267): `set_client_info` declares the connection's roles, `attach_session` and
|
|
8
|
+
* `release_attachment` take and drop a live attachment on the session's identity, `abort` runs the host's effect
|
|
9
|
+
* path and its role rule (a Stop needs `interactive_user`), and input sent with a `clientMessageId` is answered
|
|
10
|
+
* with an admission receipt that a retry gets again, marked `replayed`. A frame this file does not own answers
|
|
7
11
|
* `undefined` and falls through to the endpoint's other commands.
|
|
8
12
|
*/
|
|
9
13
|
import { randomUUID } from "node:crypto";
|
|
10
14
|
import { realpathSync } from "node:fs";
|
|
11
15
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
16
|
+
import { CONNECTION_ROLE_FIXED } from "../rpc/connection-roles.js";
|
|
17
|
+
import { INTERACTIVE_USER_CAPABILITY } from "../rpc/custom-capability.js";
|
|
18
|
+
import { runAbort, runGetEffectStatus } from "../rpc/effect-commands.js";
|
|
13
19
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
14
20
|
import { answerSubscribeSessions, answerUnsubscribeSessions, refusal } from "../rpc/session-status-commands.js";
|
|
15
21
|
import { keyOf } from "./session-control-attach.js";
|
|
22
|
+
import { failure, success } from "./session-control-server.js";
|
|
16
23
|
export const TUI_RPC_COMMANDS = [
|
|
17
24
|
"open_session",
|
|
18
25
|
"subscribe_sessions",
|
|
@@ -21,7 +28,13 @@ export const TUI_RPC_COMMANDS = [
|
|
|
21
28
|
"follow_up",
|
|
22
29
|
"prompt",
|
|
23
30
|
"get_effect_status",
|
|
31
|
+
"set_client_info",
|
|
32
|
+
"attach_session",
|
|
33
|
+
"release_attachment",
|
|
34
|
+
"abort",
|
|
24
35
|
];
|
|
36
|
+
/** The host's answer to a Stop from a connection that never declared the user's role (senpi#3811). */
|
|
37
|
+
const STOP_REQUIRES_INTERACTIVE_CLIENT = "stop_requires_interactive_client: declare interactive_user with set_client_info first";
|
|
25
38
|
/** `null` = answered on the writer already; `undefined` = not a command this file owns. */
|
|
26
39
|
export async function runTuiRpcCommand(context, connection, command) {
|
|
27
40
|
switch (command.type) {
|
|
@@ -36,6 +49,14 @@ export async function runTuiRpcCommand(context, connection, command) {
|
|
|
36
49
|
return admitInput(context, command, command.type === "steer" ? "steer" : "followUp", command.whenIdle);
|
|
37
50
|
case "prompt":
|
|
38
51
|
return admitPrompt(context, command);
|
|
52
|
+
case "set_client_info":
|
|
53
|
+
return setClientInfo(context, connection, command);
|
|
54
|
+
case "attach_session":
|
|
55
|
+
return attachSession(context, connection, command);
|
|
56
|
+
case "release_attachment":
|
|
57
|
+
return releaseAttachment(context, connection, command);
|
|
58
|
+
case "abort":
|
|
59
|
+
return abort(context, connection, frame(command));
|
|
39
60
|
case "get_effect_status":
|
|
40
61
|
return runGetEffectStatus({
|
|
41
62
|
session: context.session,
|
|
@@ -74,6 +95,68 @@ function openSession(context, connection, command) {
|
|
|
74
95
|
});
|
|
75
96
|
return null;
|
|
76
97
|
}
|
|
98
|
+
/** Declares this connection's roles, fixed once declared, exactly as on a host; the other fields change nothing here. */
|
|
99
|
+
function setClientInfo(context, connection, command) {
|
|
100
|
+
const capabilities = command.capabilities;
|
|
101
|
+
if (capabilities === undefined)
|
|
102
|
+
return success(command.id, "set_client_info");
|
|
103
|
+
if (!Array.isArray(capabilities) || !capabilities.every((entry) => typeof entry === "string"))
|
|
104
|
+
return failure(command.id, "set_client_info", "capabilities must be an array of strings");
|
|
105
|
+
if (!context.attachments.roles.declare(keyOf(connection), capabilities))
|
|
106
|
+
return failure(command.id, "set_client_info", CONNECTION_ROLE_FIXED);
|
|
107
|
+
return success(command.id, "set_client_info");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A live attachment on the session's identity, as on a host: `sessionId` is this terminal's routing handle,
|
|
111
|
+
* `durableSessionId` its session id, and `expectedHostInstance`, when sent, this endpoint's instance id. The answer
|
|
112
|
+
* follows the replay of the in-flight snapshot, then the session's records reach the connection.
|
|
113
|
+
*/
|
|
114
|
+
function attachSession(context, connection, command) {
|
|
115
|
+
const { session, attachments } = context;
|
|
116
|
+
const id = idOf(command);
|
|
117
|
+
if (command.sessionId !== attachments.handle)
|
|
118
|
+
return refusal(id, "attach_session", "unknown_session", {
|
|
119
|
+
reason: "this terminal hosts one session and the request names another",
|
|
120
|
+
action: "attach with this terminal's sessionId from list_sessions",
|
|
121
|
+
});
|
|
122
|
+
const instanceMatches = command.expectedHostInstance === undefined || command.expectedHostInstance === context.instanceId;
|
|
123
|
+
if (command.durableSessionId !== session.sessionId || !instanceMatches)
|
|
124
|
+
return refusal(id, "attach_session", "session_identity_mismatch", {
|
|
125
|
+
reason: "durableSessionId or expectedHostInstance does not name this terminal's session",
|
|
126
|
+
action: "read list_sessions and get_protocol_info again, then attach with their ids",
|
|
127
|
+
});
|
|
128
|
+
attachments.run(connection, () => {
|
|
129
|
+
attachments.attach(connection);
|
|
130
|
+
attachments.writer.enqueue(attachments.handle, {
|
|
131
|
+
id: command.id,
|
|
132
|
+
type: "response",
|
|
133
|
+
command: "attach_session",
|
|
134
|
+
success: true,
|
|
135
|
+
data: { sessionId: attachments.handle, attached: true },
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function releaseAttachment(context, connection, command) {
|
|
141
|
+
if (command.sessionId !== context.attachments.handle || !context.attachments.detach(connection))
|
|
142
|
+
return refusal(idOf(command), "release_attachment", "session_not_attached", {
|
|
143
|
+
reason: "this connection is not attached to that session",
|
|
144
|
+
action: "attach first with attach_session or open_session",
|
|
145
|
+
});
|
|
146
|
+
// Nothing outlives the terminal's own attachment, so a release is never deferred.
|
|
147
|
+
return success(command.id, "release_attachment", { deferred: false });
|
|
148
|
+
}
|
|
149
|
+
/** The host's `abort`: the same effect receipts, and a Stop only from a connection that declared `interactive_user`. */
|
|
150
|
+
async function abort(context, connection, command) {
|
|
151
|
+
const key = keyOf(connection);
|
|
152
|
+
if (command.intent === "stop" && !context.attachments.roles.has(key, INTERACTIVE_USER_CAPABILITY))
|
|
153
|
+
return failure(command.id, "abort", STOP_REQUIRES_INTERACTIVE_CLIENT);
|
|
154
|
+
const { session } = context;
|
|
155
|
+
return runAbort({ session, by: { connectionId: key }, sessionId: context.attachments.handle }, command, () => (command.intent === "stop" ? session.stop() : session.abort()).catch((cause) => {
|
|
156
|
+
// Reported out of band, as on a host; the answer never waits on it.
|
|
157
|
+
connection.send({ type: "rpc_error", error: String(cause) });
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
77
160
|
function subscribe(context, connection, command) {
|
|
78
161
|
const { writer } = context.attachments;
|
|
79
162
|
const key = keyOf(connection);
|
|
@@ -145,6 +228,22 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
145
228
|
});
|
|
146
229
|
const admission = context.session.externalAdmission;
|
|
147
230
|
const clientMessageId = typeof command.clientMessageId === "string" ? command.clientMessageId : undefined;
|
|
231
|
+
const fingerprint = JSON.stringify([command.type, message, deliverAs, whenIdle ?? null]);
|
|
232
|
+
const prior = clientMessageId === undefined ? undefined : context.attachments.receipts.get(clientMessageId);
|
|
233
|
+
if (prior !== undefined) {
|
|
234
|
+
if (prior.fingerprint !== fingerprint)
|
|
235
|
+
return refusal(id, command.type, "client_message_id_conflict", {
|
|
236
|
+
reason: `clientMessageId ${clientMessageId} was already used for different input`,
|
|
237
|
+
action: "send new input with a new clientMessageId",
|
|
238
|
+
});
|
|
239
|
+
return answered(id, command.type, {
|
|
240
|
+
disposition: prior.disposition,
|
|
241
|
+
clientMessageId,
|
|
242
|
+
replayed: true,
|
|
243
|
+
duplicate: true,
|
|
244
|
+
admission: prior.admission,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
148
247
|
const expected = typeof command.expected_turn_id === "number" ? command.expected_turn_id : admission.turnEpoch;
|
|
149
248
|
const result = admission.admit({
|
|
150
249
|
delivery_id: clientMessageId ?? `tui-rpc-${randomUUID()}`,
|
|
@@ -155,12 +254,23 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
155
254
|
whenIdle: whenIdle === "start" ? "start" : "queue",
|
|
156
255
|
});
|
|
157
256
|
const identity = clientMessageId === undefined ? {} : { clientMessageId };
|
|
257
|
+
const admitted = (disposition) => {
|
|
258
|
+
if (clientMessageId === undefined)
|
|
259
|
+
return answered(id, command.type, { disposition });
|
|
260
|
+
const receipt = {
|
|
261
|
+
fingerprint,
|
|
262
|
+
disposition,
|
|
263
|
+
admission: { clientMessageId, durableSessionId: context.session.sessionId, disposition },
|
|
264
|
+
};
|
|
265
|
+
context.attachments.receipts.set(clientMessageId, receipt);
|
|
266
|
+
return answered(id, command.type, { disposition, clientMessageId, admission: receipt.admission });
|
|
267
|
+
};
|
|
158
268
|
switch (result.kind) {
|
|
159
269
|
case "started":
|
|
160
|
-
return
|
|
270
|
+
return admitted("started");
|
|
161
271
|
case "steered":
|
|
162
272
|
case "queued":
|
|
163
|
-
return
|
|
273
|
+
return admitted("queued");
|
|
164
274
|
case "already_admitted":
|
|
165
275
|
return answered(id, command.type, { disposition: "queued", duplicate: true, ...identity });
|
|
166
276
|
case "held_draft":
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
* (`authenticateSocket`, the handshake every RPC transport shares); a wrong secret is disconnected
|
|
4
4
|
* before a single JSONL line is read. After that it is the RPC framing: one LF-terminated JSON
|
|
5
5
|
* object per request, one response per request carrying its `id`, plus pushed feed events.
|
|
6
|
+
*
|
|
7
|
+
* The socket path is the endpoint's logical address on every platform (its secret lives beside it); on win32 the
|
|
8
|
+
* listener binds the named pipe the host transport derives from that path and the secret (senpi#4267), which is
|
|
9
|
+
* the address every client already connects to.
|
|
6
10
|
*/
|
|
7
11
|
import { chmod } from "node:fs/promises";
|
|
8
12
|
import { createServer } from "node:net";
|
|
9
13
|
import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS, serializeJsonLine } from "../rpc/jsonl.js";
|
|
10
14
|
import { rpcCommandShapeError } from "../rpc/rpc-input-validation.js";
|
|
11
15
|
import { socketSink } from "../rpc/socket-sink.js";
|
|
12
|
-
import { authenticateSocket } from "../rpc/socket-transport.js";
|
|
16
|
+
import { authenticateSocket, resolveSocketTransportAddress } from "../rpc/socket-transport.js";
|
|
13
17
|
export async function listenControlSocket(socketPath, secret, handlers) {
|
|
14
18
|
const clients = new Set();
|
|
15
19
|
let nextConnection = 0;
|
|
@@ -21,13 +25,15 @@ export async function listenControlSocket(socketPath, secret, handlers) {
|
|
|
21
25
|
});
|
|
22
26
|
await new Promise((resolve, reject) => {
|
|
23
27
|
server.once("error", reject);
|
|
24
|
-
server.listen(socketPath, () => {
|
|
28
|
+
server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), () => {
|
|
25
29
|
server.off("error", reject);
|
|
26
30
|
resolve();
|
|
27
31
|
});
|
|
28
32
|
});
|
|
29
33
|
server.on("error", (error) => handlers.listenerError(error));
|
|
30
|
-
|
|
34
|
+
// A named pipe has no file to restrict: the secret, 0600 beside the logical path, is its gate.
|
|
35
|
+
if (process.platform !== "win32")
|
|
36
|
+
await chmod(socketPath, 0o600);
|
|
31
37
|
return { close: () => closeServer(server, clients) };
|
|
32
38
|
}
|
|
33
39
|
function acceptConnection(socket, id, handlers) {
|
|
@@ -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;
|