@mono-agent/agent-app 0.6.1 → 0.8.0
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/README.md +210 -17
- package/dist/adapter-send-proxy.d.ts +18 -0
- package/dist/adapter-send-proxy.d.ts.map +1 -0
- package/dist/adapter-send-proxy.js +229 -0
- package/dist/adapter-send-proxy.js.map +1 -0
- package/dist/adapter-send-tools-main.js +62 -4
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +41 -8
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +563 -108
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +8 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +532 -134
- package/dist/app.js.map +1 -1
- package/dist/broadcast-recorder.d.ts.map +1 -1
- package/dist/broadcast-recorder.js +24 -2
- package/dist/broadcast-recorder.js.map +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +18 -7
- package/dist/channels.js.map +1 -1
- package/dist/cli.d.ts +77 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2106 -99
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +195 -6
- package/dist/config-reference.js.map +1 -1
- package/dist/configuration-proposal-main.d.ts +3 -0
- package/dist/configuration-proposal-main.d.ts.map +1 -0
- package/dist/configuration-proposal-main.js +13 -0
- package/dist/configuration-proposal-main.js.map +1 -0
- package/dist/configuration-proposal-tool.d.ts +26 -0
- package/dist/configuration-proposal-tool.d.ts.map +1 -0
- package/dist/configuration-proposal-tool.js +139 -0
- package/dist/configuration-proposal-tool.js.map +1 -0
- package/dist/configured-agent.d.ts +12 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +150 -18
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts +3 -0
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +44 -1
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor.d.ts +27 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1198 -96
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-managed-memory.d.ts +39 -0
- package/dist/first-run-managed-memory.d.ts.map +1 -0
- package/dist/first-run-managed-memory.js +521 -0
- package/dist/first-run-managed-memory.js.map +1 -0
- package/dist/first-run-readiness.d.ts +111 -0
- package/dist/first-run-readiness.d.ts.map +1 -0
- package/dist/first-run-readiness.js +757 -0
- package/dist/first-run-readiness.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +88 -3
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1212 -23
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +32 -2
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +309 -28
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/local-configuration.d.ts +56 -0
- package/dist/local-configuration.d.ts.map +1 -0
- package/dist/local-configuration.js +914 -0
- package/dist/local-configuration.js.map +1 -0
- package/dist/memory-command.d.ts +4 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +751 -27
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall.d.ts +38 -10
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +82 -25
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-retrieval.d.ts +66 -0
- package/dist/memory-retrieval.d.ts.map +1 -0
- package/dist/memory-retrieval.js +332 -0
- package/dist/memory-retrieval.js.map +1 -0
- package/dist/modules/base.d.ts +6 -5
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js +11 -7
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/catalog.d.ts.map +1 -1
- package/dist/modules/catalog.js +54 -20
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/index.d.ts +2 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/known-tools.d.ts +3 -0
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +11 -1
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/modules/types.d.ts +20 -6
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/package-version.d.ts +3 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +14 -0
- package/dist/package-version.js.map +1 -0
- package/dist/pi-auth-store-inspection.d.ts +21 -0
- package/dist/pi-auth-store-inspection.d.ts.map +1 -0
- package/dist/pi-auth-store-inspection.js +120 -0
- package/dist/pi-auth-store-inspection.js.map +1 -0
- package/dist/pi-oauth-login-main.d.ts +3 -0
- package/dist/pi-oauth-login-main.d.ts.map +1 -0
- package/dist/pi-oauth-login-main.js +14 -0
- package/dist/pi-oauth-login-main.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +22 -0
- package/dist/pi-oauth-login.d.ts.map +1 -0
- package/dist/pi-oauth-login.js +79 -0
- package/dist/pi-oauth-login.js.map +1 -0
- package/dist/project-skills.d.ts +40 -0
- package/dist/project-skills.d.ts.map +1 -0
- package/dist/project-skills.js +573 -0
- package/dist/project-skills.js.map +1 -0
- package/dist/provider-setup.d.ts +136 -5
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +1552 -33
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +12 -0
- package/dist/readiness-probe-worker.d.ts.map +1 -0
- package/dist/readiness-probe-worker.js +276 -0
- package/dist/readiness-probe-worker.js.map +1 -0
- package/dist/readiness-probe.d.ts +100 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +780 -0
- package/dist/readiness-probe.js.map +1 -0
- package/dist/request-model-override.d.ts +43 -1
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +178 -19
- package/dist/request-model-override.js.map +1 -1
- package/dist/run-history.d.ts +32 -0
- package/dist/run-history.d.ts.map +1 -0
- package/dist/run-history.js +859 -0
- package/dist/run-history.js.map +1 -0
- package/dist/runs-health.d.ts.map +1 -1
- package/dist/runs-health.js +12 -4
- package/dist/runs-health.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +14 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -0
- package/dist/runtime-option-extensions.js +98 -0
- package/dist/runtime-option-extensions.js.map +1 -0
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +13 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/sandbox-manager.d.ts +69 -0
- package/dist/sandbox-manager.d.ts.map +1 -0
- package/dist/sandbox-manager.js +856 -0
- package/dist/sandbox-manager.js.map +1 -0
- package/dist/supermemory-plugin.d.ts +54 -0
- package/dist/supermemory-plugin.d.ts.map +1 -0
- package/dist/supermemory-plugin.js +136 -0
- package/dist/supermemory-plugin.js.map +1 -0
- package/dist/tui-command.d.ts +15 -0
- package/dist/tui-command.d.ts.map +1 -1
- package/dist/tui-command.js +46 -6
- package/dist/tui-command.js.map +1 -1
- package/dist/web-command.d.ts +8 -0
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +174 -17
- package/dist/web-command.js.map +1 -1
- package/dist/wizard/answers.d.ts +31 -4
- package/dist/wizard/answers.d.ts.map +1 -1
- package/dist/wizard/answers.js +145 -16
- package/dist/wizard/answers.js.map +1 -1
- package/dist/wizard/from-flags.d.ts +7 -0
- package/dist/wizard/from-flags.d.ts.map +1 -1
- package/dist/wizard/from-flags.js +36 -2
- package/dist/wizard/from-flags.js.map +1 -1
- package/dist/wizard/index.d.ts +4 -4
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +2 -2
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/model-discovery.d.ts +38 -3
- package/dist/wizard/model-discovery.d.ts.map +1 -1
- package/dist/wizard/model-discovery.js +927 -102
- package/dist/wizard/model-discovery.js.map +1 -1
- package/dist/wizard/presets.d.ts +1 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +7 -12
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/prompts.d.ts +66 -11
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +190 -20
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.d.ts +60 -3
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +1161 -145
- package/dist/wizard/run.js.map +1 -1
- package/package.json +21 -16
- package/resources/srt/package-lock.json +66 -0
- package/resources/srt/package.json +9 -0
- package/schema/mono-agent.config.schema.json +381 -17
- package/skills/mono-agent-composer/SKILL.md +4 -4
- package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
- package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +8 -7
- package/skills/mono-agent-composer/references/validation.md +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { splitTextByCodePoints } from "@mono-agent/agent-contracts";
|
|
6
8
|
import { ALLOW_ALL_TOOLS } from "@mono-agent/config";
|
|
7
9
|
import * as z from "zod/v4";
|
|
8
10
|
import { LEGACY_TOOL_ALIASES } from "./modules/known-tools.js";
|
|
@@ -11,6 +13,9 @@ let slackModule;
|
|
|
11
13
|
let telegramModule;
|
|
12
14
|
const loadSlackModule = async () => (slackModule ??= await import("@mono-agent/slack-adapter"));
|
|
13
15
|
const loadTelegramModule = async () => (telegramModule ??= await import("@mono-agent/telegram-adapter"));
|
|
16
|
+
const SLACK_SEND_MESSAGE_MAX_CHARS = 40_000;
|
|
17
|
+
/** Keep cancellation responsive even when the loopback bridge is wedged. */
|
|
18
|
+
const ASK_BRIDGE_CLEANUP_TIMEOUT_MS = 1_000;
|
|
14
19
|
/**
|
|
15
20
|
* Model-visible send tools for explicitly allowed, already-enabled communication adapters.
|
|
16
21
|
*
|
|
@@ -24,9 +29,13 @@ const loadTelegramModule = async () => (telegramModule ??= await import("@mono-a
|
|
|
24
29
|
export const ADAPTER_SEND_TOOLS_MCP_SERVER_NAME = "mono-agent-adapter-send";
|
|
25
30
|
export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
26
31
|
const telegramSendAllowed = isAdapterToolAllowed("TelegramSendMessage", options);
|
|
27
|
-
const telegramAskAllowed =
|
|
32
|
+
const telegramAskAllowed = options.suppressInteractionTools !== true
|
|
33
|
+
&& isAdapterToolAllowed("TelegramAskButtons", options);
|
|
28
34
|
const telegramFileAllowed = isAdapterToolAllowed("TelegramSendFile", options);
|
|
29
35
|
const telegramAnyAllowed = telegramSendAllowed || telegramAskAllowed || telegramFileAllowed;
|
|
36
|
+
const telegramAskBridge = telegramAskAllowed
|
|
37
|
+
? resolveAskUserToolSettings(input.env, options.interaction)
|
|
38
|
+
: undefined;
|
|
30
39
|
const [slack, telegram] = await Promise.all([
|
|
31
40
|
isAdapterToolAllowed("SlackSendMessage", options)
|
|
32
41
|
? resolveSlackSendToolSettings(input, options)
|
|
@@ -36,11 +45,11 @@ export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
|
36
45
|
send: telegramSendAllowed,
|
|
37
46
|
ask: telegramAskAllowed,
|
|
38
47
|
file: telegramFileAllowed,
|
|
39
|
-
})
|
|
48
|
+
}, telegramAskBridge)
|
|
40
49
|
: undefined,
|
|
41
50
|
]);
|
|
42
|
-
const askUser = isAdapterToolAllowed("AskUser", options)
|
|
43
|
-
? resolveAskUserToolSettings(input.env)
|
|
51
|
+
const askUser = options.suppressInteractionTools !== true && isAdapterToolAllowed("AskUser", options)
|
|
52
|
+
? resolveAskUserToolSettings(input.env, options.interaction)
|
|
44
53
|
: undefined;
|
|
45
54
|
if (slack === undefined && telegram === undefined && askUser === undefined) {
|
|
46
55
|
return undefined;
|
|
@@ -56,20 +65,23 @@ export async function resolveAdapterSendToolsSettings(input, options = {}) {
|
|
|
56
65
|
* into the environment (URL + bearer token). The producing conversation id is
|
|
57
66
|
* per-request env, present in the spawned child.
|
|
58
67
|
*/
|
|
59
|
-
function resolveAskUserToolSettings(env) {
|
|
60
|
-
const bridgeUrl = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_URL);
|
|
61
|
-
const bridgeToken = optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_TOKEN);
|
|
68
|
+
function resolveAskUserToolSettings(env, interaction) {
|
|
69
|
+
const bridgeUrl = interaction?.bridgeUrl ?? optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_URL);
|
|
70
|
+
const bridgeToken = interaction?.bridgeToken ?? optionalString(env.MONO_AGENT_INTERACTION_BRIDGE_TOKEN);
|
|
62
71
|
if (bridgeUrl === undefined || bridgeToken === undefined) {
|
|
63
72
|
return undefined;
|
|
64
73
|
}
|
|
65
74
|
const timeoutRaw = Number(optionalString(env.MONO_AGENT_ASK_USER_TIMEOUT_MS));
|
|
66
|
-
const timeoutMs =
|
|
75
|
+
const timeoutMs = interaction?.timeoutMs
|
|
76
|
+
?? (Number.isFinite(timeoutRaw) && timeoutRaw >= 1000 ? timeoutRaw : 600_000);
|
|
67
77
|
const conversationId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
|
|
78
|
+
const runId = optionalString(env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID);
|
|
68
79
|
return {
|
|
69
80
|
bridgeUrl,
|
|
70
81
|
bridgeToken,
|
|
71
82
|
timeoutMs,
|
|
72
83
|
...(conversationId === undefined ? {} : { conversationId }),
|
|
84
|
+
...(runId === undefined ? {} : { runId }),
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
export function adapterSendToolNames(settings) {
|
|
@@ -99,16 +111,35 @@ export function adapterSendToolNames(settings) {
|
|
|
99
111
|
export function isAdapterSendToolAllowed(name, policy) {
|
|
100
112
|
return isAdapterToolAllowed(name, policy);
|
|
101
113
|
}
|
|
102
|
-
export function adapterSendToolsMcpEnv(configPath, allowedTools, context) {
|
|
114
|
+
export function adapterSendToolsMcpEnv(configPath, allowedTools, context, interaction) {
|
|
103
115
|
return {
|
|
104
116
|
MONO_AGENT_ADAPTER_TOOLS_CONFIG_PATH: configPath,
|
|
105
117
|
MONO_AGENT_ADAPTER_TOOLS_ALLOWED_TOOLS: JSON.stringify(allowedTools),
|
|
106
118
|
...(context?.conversationId === undefined
|
|
107
119
|
? {}
|
|
108
120
|
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID: context.conversationId }),
|
|
121
|
+
...(context?.runId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { MONO_AGENT_ADAPTER_TOOLS_PRODUCING_RUN_ID: context.runId }),
|
|
109
124
|
...(context?.indexPath === undefined
|
|
110
125
|
? {}
|
|
111
126
|
: { MONO_AGENT_ADAPTER_TOOLS_POST_INDEX_PATH: context.indexPath }),
|
|
127
|
+
...(context?.runOutputDir === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DIR: context.runOutputDir }),
|
|
130
|
+
...(context?.runOutputIdentity === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: {
|
|
133
|
+
MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DEV: String(context.runOutputIdentity.dev),
|
|
134
|
+
MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_INO: String(context.runOutputIdentity.ino),
|
|
135
|
+
}),
|
|
136
|
+
...(interaction === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: {
|
|
139
|
+
MONO_AGENT_INTERACTION_BRIDGE_URL: interaction.bridgeUrl,
|
|
140
|
+
MONO_AGENT_INTERACTION_BRIDGE_TOKEN: interaction.bridgeToken,
|
|
141
|
+
MONO_AGENT_ASK_USER_TIMEOUT_MS: String(interaction.timeoutMs),
|
|
142
|
+
}),
|
|
112
143
|
};
|
|
113
144
|
}
|
|
114
145
|
export function adapterSendToolsChildConfigFromEnv(env, cwd) {
|
|
@@ -126,14 +157,27 @@ export function adapterSendToolsChildConfigFromEnv(env, cwd) {
|
|
|
126
157
|
...(indexing === undefined ? {} : { indexing }),
|
|
127
158
|
};
|
|
128
159
|
}
|
|
129
|
-
export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context) {
|
|
130
|
-
|
|
160
|
+
export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context, interaction) {
|
|
161
|
+
const spec = {
|
|
131
162
|
type: "stdio",
|
|
132
163
|
command: process.execPath,
|
|
133
|
-
|
|
164
|
+
// Node 24 on macOS defaults to the system trust store, whose trustd access
|
|
165
|
+
// is intentionally unavailable inside strict SRT. Force Node's bundled CA
|
|
166
|
+
// set for this child while still honoring NODE_EXTRA_CA_CERTS when SRT TLS
|
|
167
|
+
// termination or an operator supplies an additional root.
|
|
168
|
+
args: ["--use-bundled-ca", fileURLToPath(new URL("./adapter-send-tools-main.js", import.meta.url))],
|
|
134
169
|
cwd,
|
|
135
|
-
env: adapterSendToolsMcpEnv(configPath, allowedTools, context),
|
|
170
|
+
env: adapterSendToolsMcpEnv(configPath, allowedTools, context, interaction),
|
|
136
171
|
};
|
|
172
|
+
// Only the trusted app-owned adapter child receives SRT's coarse loopback
|
|
173
|
+
// capability. A symbol cannot be supplied by JSON MCP config and disappears
|
|
174
|
+
// from serialized provider/tool metadata, so ordinary Bash/project MCP
|
|
175
|
+
// processes keep the stricter no-bind allowlist policy.
|
|
176
|
+
Object.defineProperty(spec, Symbol.for("@mono-agent/app-owned-local-binding"), {
|
|
177
|
+
value: true,
|
|
178
|
+
enumerable: false,
|
|
179
|
+
});
|
|
180
|
+
return spec;
|
|
137
181
|
}
|
|
138
182
|
/**
|
|
139
183
|
* Per-request runtime extension that injects the adapter-send stdio MCP server. It
|
|
@@ -141,43 +185,65 @@ export function adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, con
|
|
|
141
185
|
* configured, forwards both to the child so a `SlackSendMessage` post is linked
|
|
142
186
|
* back to this conversation (so a later in-thread reply resumes it).
|
|
143
187
|
*/
|
|
144
|
-
export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath) {
|
|
188
|
+
export function createAdapterSendToolsRuntimeExtension(configPath, cwd, allowedTools, indexPath, interaction, runOutputRoot) {
|
|
145
189
|
return async (input) => {
|
|
146
190
|
const conversationId = input?.request?.conversationId;
|
|
191
|
+
const runId = input?.runId;
|
|
147
192
|
const hasConversation = typeof conversationId === "string" && conversationId.trim().length > 0;
|
|
193
|
+
const hasRunId = typeof runId === "string" && runId.trim().length > 0;
|
|
194
|
+
const runOutput = runOutputRoot === undefined || !hasRunId
|
|
195
|
+
? undefined
|
|
196
|
+
: await ensureAdapterRunOutputDir(runOutputRoot, runId);
|
|
148
197
|
// The conversation id is forwarded whenever known — AskUser targets it even
|
|
149
198
|
// without indexing; the index path additionally enables posted-message links.
|
|
150
|
-
const context = hasConversation
|
|
151
|
-
? {
|
|
199
|
+
const context = hasConversation || hasRunId
|
|
200
|
+
? {
|
|
201
|
+
...(hasConversation ? { conversationId } : {}),
|
|
202
|
+
...(hasRunId ? { runId } : {}),
|
|
203
|
+
...(indexPath === undefined ? {} : { indexPath }),
|
|
204
|
+
...(runOutput === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { runOutputDir: runOutput.path, runOutputIdentity: runOutput.identity }),
|
|
207
|
+
}
|
|
152
208
|
: undefined;
|
|
153
209
|
return {
|
|
154
210
|
runtimeOptions: {
|
|
155
211
|
mcpServers: {
|
|
156
|
-
[ADAPTER_SEND_TOOLS_MCP_SERVER_NAME]: adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context),
|
|
212
|
+
[ADAPTER_SEND_TOOLS_MCP_SERVER_NAME]: adapterSendToolsMcpServerSpec(configPath, cwd, allowedTools, context, interaction),
|
|
157
213
|
},
|
|
158
214
|
},
|
|
159
215
|
cleanup: async () => { },
|
|
216
|
+
settleCleanup: async () => {
|
|
217
|
+
if (runOutput !== undefined) {
|
|
218
|
+
await removeOwnedDirectory(runOutput.path, runOutput.identity);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
160
221
|
};
|
|
161
222
|
};
|
|
162
223
|
}
|
|
163
|
-
export async function createAdapterSendToolsClients(settings) {
|
|
224
|
+
export async function createAdapterSendToolsClients(settings, options = {}) {
|
|
164
225
|
const slack = settings.slack === undefined
|
|
165
226
|
? undefined
|
|
166
|
-
: new (await loadSlackModule()).SlackWebApiClient({
|
|
227
|
+
: new (await loadSlackModule()).SlackWebApiClient({
|
|
228
|
+
botToken: settings.slack.botToken,
|
|
229
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
230
|
+
});
|
|
167
231
|
const telegram = settings.telegram === undefined
|
|
168
232
|
? undefined
|
|
169
233
|
: (await loadTelegramModule()).createTelegramMessageSender(settings.telegram.botToken, {
|
|
170
234
|
...(settings.telegram.apiRoot === undefined ? {} : { apiRoot: settings.telegram.apiRoot }),
|
|
235
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
171
236
|
});
|
|
172
237
|
return {
|
|
173
238
|
...(slack === undefined ? {} : { slack }),
|
|
174
239
|
...(telegram === undefined ? {} : { telegram }),
|
|
175
240
|
};
|
|
176
241
|
}
|
|
177
|
-
export async function createAdapterSendToolsServer(settings, clients, indexing) {
|
|
242
|
+
export async function createAdapterSendToolsServer(settings, clients, indexing, options = {}) {
|
|
178
243
|
const server = new McpServer({ name: "agent-adapter-send-tools", version: "0.3.0" });
|
|
179
244
|
if (settings.slack !== undefined && clients.slack !== undefined) {
|
|
180
|
-
|
|
245
|
+
const adapter = await loadSlackModule();
|
|
246
|
+
registerSlackSendTool(server, settings.slack, clients.slack, adapter.formatMarkdownForSlack, indexing);
|
|
181
247
|
}
|
|
182
248
|
if (settings.telegram !== undefined && clients.telegram !== undefined) {
|
|
183
249
|
// Loaded once here (not per-register-call) because registerTelegramAskTool
|
|
@@ -188,7 +254,7 @@ export async function createAdapterSendToolsServer(settings, clients, indexing)
|
|
|
188
254
|
registerTelegramSendTool(server, settings.telegram, clients.telegram);
|
|
189
255
|
}
|
|
190
256
|
if (settings.telegram.tools.ask) {
|
|
191
|
-
registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter);
|
|
257
|
+
registerTelegramAskTool(server, settings.telegram, clients.telegram, adapter, options.fetchImpl ?? globalThis.fetch);
|
|
192
258
|
}
|
|
193
259
|
if (settings.telegram.tools.file) {
|
|
194
260
|
registerTelegramSendFileTool(server, settings.telegram, clients.telegram, adapter);
|
|
@@ -197,13 +263,13 @@ export async function createAdapterSendToolsServer(settings, clients, indexing)
|
|
|
197
263
|
// AskUser needs a target conversation; the parent app process resolves the
|
|
198
264
|
// settings without one (for tool-name gating) and must not register the tool.
|
|
199
265
|
if (settings.askUser?.conversationId !== undefined) {
|
|
200
|
-
registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId });
|
|
266
|
+
registerAskUserTool(server, { ...settings.askUser, conversationId: settings.askUser.conversationId }, options.fetchImpl ?? globalThis.fetch);
|
|
201
267
|
}
|
|
202
268
|
return server;
|
|
203
269
|
}
|
|
204
270
|
/** Long-poll wait per bridge request; the overall wait is bounded server-side by the ask's timeout. */
|
|
205
271
|
const ASK_USER_POLL_WAIT_MS = 20_000;
|
|
206
|
-
function registerAskUserTool(server, settings) {
|
|
272
|
+
function registerAskUserTool(server, settings, fetchImpl) {
|
|
207
273
|
server.registerTool("AskUser", {
|
|
208
274
|
title: "Ask the user and wait",
|
|
209
275
|
description: "Ask the user ONE consolidated free-text question on the current conversation and WAIT for their reply (blocking, up to the configured timeout — default 10 minutes). Returns the user's answer text. Consolidate everything you need into a single question — a second concurrent ask fails. If the wait times out, the tool returns without an answer and the user's late reply will arrive as their next message: finish the turn gracefully (proceed with sensible defaults and say what you assumed).",
|
|
@@ -211,63 +277,100 @@ function registerAskUserTool(server, settings) {
|
|
|
211
277
|
question: z.string().min(1).describe("The full question to show the user (plain text, no markdown)."),
|
|
212
278
|
},
|
|
213
279
|
}, async (args, extra) => {
|
|
214
|
-
const created = await askBridgeRequest(settings, "POST", "/v1/asks", {
|
|
280
|
+
const created = await askBridgeRequest(settings, fetchImpl, "POST", "/v1/asks", {
|
|
215
281
|
conversationId: settings.conversationId,
|
|
282
|
+
producerConversationId: settings.conversationId,
|
|
283
|
+
runId: settings.runId,
|
|
284
|
+
toolName: "AskUser",
|
|
216
285
|
question: args.question,
|
|
217
286
|
timeoutMs: settings.timeoutMs,
|
|
218
|
-
});
|
|
287
|
+
}, extra.signal);
|
|
219
288
|
if (created.status === 409) {
|
|
220
|
-
return
|
|
289
|
+
return askToolResult("A question is already pending for the user. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" });
|
|
221
290
|
}
|
|
222
291
|
if (created.status === 501) {
|
|
223
|
-
return
|
|
292
|
+
return askToolResult("This conversation's channel does not support interactive asks. Ask your question in your final reply instead.", { answered: false, reason: "unsupported_channel" });
|
|
224
293
|
}
|
|
225
294
|
if (created.status !== 201 || typeof created.body.askId !== "string") {
|
|
226
295
|
throw new Error(`AskUser: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
|
|
227
296
|
}
|
|
228
|
-
|
|
229
|
-
|
|
297
|
+
return await awaitBridgeAsk(settings, created.body.askId, "AskUser", extra, fetchImpl, extra.signal);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function askToolResult(text, structured, extraStructured = {}) {
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: "text", text }],
|
|
303
|
+
structuredContent: { ok: true, ...extraStructured, ...structured },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function awaitBridgeAsk(settings, askId, toolName, extra, fetchImpl, signal, extraStructured = {}) {
|
|
307
|
+
const startedMs = Date.now();
|
|
308
|
+
try {
|
|
230
309
|
for (;;) {
|
|
231
|
-
const poll = await askBridgeRequest(settings, "GET", `/v1/asks/${encodeURIComponent(askId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}
|
|
310
|
+
const poll = await askBridgeRequest(settings, fetchImpl, "GET", `/v1/asks/${encodeURIComponent(askId)}?waitMs=${String(ASK_USER_POLL_WAIT_MS)}`, undefined, signal);
|
|
232
311
|
if (poll.status !== 200) {
|
|
233
|
-
throw new Error(
|
|
312
|
+
throw new Error(`${toolName}: lost the pending ask (HTTP ${String(poll.status)}).`);
|
|
234
313
|
}
|
|
235
314
|
// Keep-alive: progress notifications reset the runtime's MCP inactivity
|
|
236
315
|
// timeout so a long human wait cannot kill the tool call.
|
|
237
316
|
await sendAskProgress(extra, Math.round((Date.now() - startedMs) / 1000));
|
|
238
317
|
const status = poll.body.status;
|
|
239
318
|
if (status === "answered" && typeof poll.body.answer === "string") {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
});
|
|
319
|
+
const answeredText = toolName === "TelegramAskButtons"
|
|
320
|
+
? `The user tapped:\n${poll.body.answer}`
|
|
321
|
+
: `The user answered:\n${poll.body.answer}`;
|
|
322
|
+
return askToolResult(answeredText, { answered: true, answer: poll.body.answer }, extraStructured);
|
|
244
323
|
}
|
|
245
324
|
if (status === "expired") {
|
|
246
|
-
return
|
|
325
|
+
return askToolResult("The user did not answer within the wait window. Their reply will arrive as their next message — wrap up this turn gracefully (proceed with sensible defaults and say what you assumed).", { answered: false, reason: "timeout" }, extraStructured);
|
|
247
326
|
}
|
|
248
327
|
if (status === "cancelled") {
|
|
249
|
-
return
|
|
328
|
+
return askToolResult("The user cancelled the current run. Stop this task.", {
|
|
250
329
|
answered: false,
|
|
251
330
|
reason: "cancelled",
|
|
252
|
-
});
|
|
331
|
+
}, extraStructured);
|
|
253
332
|
}
|
|
254
333
|
}
|
|
255
|
-
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
// A cancelled MCP call must not leave a bridge ask pending until its full
|
|
337
|
+
// human timeout. Cleanup gets its own bounded signal (not the aborted call
|
|
338
|
+
// signal), and its failure must never replace the primary tool failure.
|
|
339
|
+
await cleanupBridgeAskBestEffort(settings, fetchImpl, askId);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
256
342
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
343
|
+
async function cleanupBridgeAskBestEffort(settings, fetchImpl, askId) {
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
let deadlineTimer;
|
|
346
|
+
const deadline = new Promise((resolve) => {
|
|
347
|
+
deadlineTimer = setTimeout(() => {
|
|
348
|
+
controller.abort(new Error("interaction bridge ask cleanup timed out"));
|
|
349
|
+
resolve();
|
|
350
|
+
}, ASK_BRIDGE_CLEANUP_TIMEOUT_MS);
|
|
351
|
+
deadlineTimer.unref?.();
|
|
352
|
+
});
|
|
353
|
+
// Attach the rejection handler before racing. If a test seam or nonstandard
|
|
354
|
+
// fetch ignores abort and rejects later, it cannot become an unhandled error.
|
|
355
|
+
const cleanup = askBridgeRequest(settings, fetchImpl, "DELETE", `/v1/asks/${encodeURIComponent(askId)}`, undefined, controller.signal).then(() => undefined, () => undefined);
|
|
356
|
+
try {
|
|
357
|
+
await Promise.race([cleanup, deadline]);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
if (deadlineTimer !== undefined) {
|
|
361
|
+
clearTimeout(deadlineTimer);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
262
364
|
}
|
|
263
|
-
async function askBridgeRequest(settings, method, path, body) {
|
|
264
|
-
const response = await
|
|
365
|
+
async function askBridgeRequest(settings, fetchImpl, method, path, body, signal) {
|
|
366
|
+
const response = await fetchImpl(new URL(path, settings.bridgeUrl), {
|
|
265
367
|
method,
|
|
266
368
|
headers: {
|
|
267
369
|
authorization: `Bearer ${settings.bridgeToken}`,
|
|
268
370
|
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
269
371
|
},
|
|
270
372
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
373
|
+
...(signal === undefined ? {} : { signal }),
|
|
271
374
|
});
|
|
272
375
|
let parsed;
|
|
273
376
|
try {
|
|
@@ -301,41 +404,64 @@ async function sendAskProgress(extra, elapsedSeconds) {
|
|
|
301
404
|
// Keep-alive only; a lost notification must never fail the ask.
|
|
302
405
|
}
|
|
303
406
|
}
|
|
304
|
-
function registerSlackSendTool(server, settings, client, indexing) {
|
|
407
|
+
function registerSlackSendTool(server, settings, client, formatMarkdownForSlack, indexing) {
|
|
305
408
|
server.registerTool("SlackSendMessage", {
|
|
306
409
|
title: "Send Slack message",
|
|
307
410
|
description: "Send a message to an allowed Slack channel or DM ID using the configured Slack adapter bot token.",
|
|
308
411
|
inputSchema: {
|
|
309
412
|
channel: z.string().min(1).describe("Slack channel or DM ID, e.g. C123456 or D123456."),
|
|
310
|
-
text: z.string().min(1).describe("Message text to send."),
|
|
413
|
+
text: z.string().min(1).describe("Message text to send. Defaults to standard Markdown converted to Slack mrkdwn."),
|
|
311
414
|
thread_ts: z.string().min(1).optional().describe("Optional Slack thread timestamp to reply in."),
|
|
312
|
-
mrkdwn: z.boolean().optional().describe("Whether Slack mrkdwn formatting is enabled."),
|
|
415
|
+
mrkdwn: z.boolean().optional().describe("Whether Slack mrkdwn formatting is enabled. Defaults to true; set false to send plain text unchanged."),
|
|
313
416
|
unfurl_links: z.boolean().optional().describe("Whether Slack should unfurl links."),
|
|
314
417
|
unfurl_media: z.boolean().optional().describe("Whether Slack should unfurl media."),
|
|
315
418
|
},
|
|
316
|
-
}, async (args) => {
|
|
419
|
+
}, async (args, extra) => {
|
|
317
420
|
assertSlackChannelAllowed(settings, args.channel);
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
421
|
+
const mrkdwn = args.mrkdwn ?? true;
|
|
422
|
+
const text = mrkdwn ? formatMarkdownForSlack(args.text) : args.text;
|
|
423
|
+
const chunks = splitTextByCodePoints(text, SLACK_SEND_MESSAGE_MAX_CHARS);
|
|
424
|
+
const results = [];
|
|
425
|
+
for (const chunk of chunks) {
|
|
426
|
+
const result = await client.chatPostMessage({
|
|
427
|
+
channel: args.channel.trim(),
|
|
428
|
+
text: chunk,
|
|
429
|
+
...(args.thread_ts === undefined ? {} : { thread_ts: args.thread_ts }),
|
|
430
|
+
mrkdwn,
|
|
431
|
+
...(args.unfurl_links === undefined ? {} : { unfurl_links: args.unfurl_links }),
|
|
432
|
+
...(args.unfurl_media === undefined ? {} : { unfurl_media: args.unfurl_media }),
|
|
433
|
+
}, { signal: extra.signal });
|
|
434
|
+
results.push(result);
|
|
435
|
+
// Link every posted chunk back to this conversation so an in-thread reply can
|
|
436
|
+
// resume it. Best-effort: appendPostedMessage never throws, so a failed index
|
|
437
|
+
// write can never fail the send.
|
|
438
|
+
if (indexing !== undefined) {
|
|
439
|
+
await appendPostedMessage(indexing.indexPath, {
|
|
440
|
+
channelId: result.channel,
|
|
441
|
+
ts: result.ts,
|
|
442
|
+
conversationId: indexing.conversationId,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const [firstResult] = results;
|
|
447
|
+
if (firstResult === undefined) {
|
|
448
|
+
throw new Error("SlackSendMessage: no message chunks were produced.");
|
|
335
449
|
}
|
|
450
|
+
const chunkRefs = results.map((result) => ({ channel: result.channel, ts: result.ts }));
|
|
451
|
+
const message = results.length === 1
|
|
452
|
+
? `Sent Slack message to ${firstResult.channel} at ${firstResult.ts}.`
|
|
453
|
+
: `Sent ${String(results.length)} Slack messages to ${firstResult.channel} starting at ${firstResult.ts}.`;
|
|
336
454
|
return {
|
|
337
|
-
content: [{ type: "text", text:
|
|
338
|
-
structuredContent:
|
|
455
|
+
content: [{ type: "text", text: message }],
|
|
456
|
+
structuredContent: results.length === 1
|
|
457
|
+
? { ok: true, channel: firstResult.channel, ts: firstResult.ts }
|
|
458
|
+
: {
|
|
459
|
+
ok: true,
|
|
460
|
+
channel: firstResult.channel,
|
|
461
|
+
ts: firstResult.ts,
|
|
462
|
+
chunkCount: results.length,
|
|
463
|
+
chunks: chunkRefs,
|
|
464
|
+
},
|
|
339
465
|
};
|
|
340
466
|
});
|
|
341
467
|
}
|
|
@@ -350,7 +476,7 @@ function registerTelegramSendTool(server, settings, client) {
|
|
|
350
476
|
reply_to_message_id: z.number().int().optional().describe("Optional message id to reply to."),
|
|
351
477
|
disable_web_page_preview: z.boolean().optional().describe("Disable Telegram link previews."),
|
|
352
478
|
},
|
|
353
|
-
}, async (args) => {
|
|
479
|
+
}, async (args, extra) => {
|
|
354
480
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
355
481
|
const result = await client.sendMessage({
|
|
356
482
|
chat_id: args.chat_id,
|
|
@@ -358,17 +484,89 @@ function registerTelegramSendTool(server, settings, client) {
|
|
|
358
484
|
...(args.parse_mode === undefined ? {} : { parse_mode: args.parse_mode }),
|
|
359
485
|
...(args.reply_to_message_id === undefined ? {} : { reply_to_message_id: args.reply_to_message_id }),
|
|
360
486
|
...(args.disable_web_page_preview === undefined ? {} : { disable_web_page_preview: args.disable_web_page_preview }),
|
|
361
|
-
});
|
|
487
|
+
}, { signal: extra.signal });
|
|
362
488
|
return {
|
|
363
489
|
content: [{ type: "text", text: `Sent Telegram message ${result.message_id} to ${String(result.chat.id)}.` }],
|
|
364
490
|
structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id },
|
|
365
491
|
};
|
|
366
492
|
});
|
|
367
493
|
}
|
|
368
|
-
|
|
494
|
+
/** Marker appended (buttons kept) when a Telegram ask times out. */
|
|
495
|
+
const TELEGRAM_ASK_EXPIRED_MARKER = "⌛ Expired — tapping a button still reaches me and starts a new turn.";
|
|
496
|
+
/** Marker appended (buttons kept) when a Telegram ask is cancelled. */
|
|
497
|
+
const TELEGRAM_ASK_CANCELLED_MARKER = "🚫 Cancelled — tapping a button still reaches me and starts a new turn.";
|
|
498
|
+
/** Bounded own-signal budget for the best-effort expiry edit — never the aborted call signal. */
|
|
499
|
+
const TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS = 2_000;
|
|
500
|
+
/** A Telegram 400 is the parse-entity rejection we fall back from (raw text, no parse_mode). */
|
|
501
|
+
function isTelegramParseError(error, adapter) {
|
|
502
|
+
return error instanceof adapter.TelegramApiError && error.errorCode === 400;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Send the ask keyboard rendered as MarkdownV2, retrying ONCE with the raw source and
|
|
506
|
+
* no parse_mode when Telegram rejects the entities with a 400 parse error. Any other
|
|
507
|
+
* failure propagates so the caller can clean up a pending bridge ask.
|
|
508
|
+
*/
|
|
509
|
+
async function sendTelegramAskKeyboard(client, adapter, params) {
|
|
510
|
+
try {
|
|
511
|
+
return await client.sendMessage({
|
|
512
|
+
chat_id: params.chat_id,
|
|
513
|
+
text: params.rendered,
|
|
514
|
+
parse_mode: "MarkdownV2",
|
|
515
|
+
reply_markup: params.replyMarkup,
|
|
516
|
+
}, { signal: params.signal });
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
if (!isTelegramParseError(error, adapter)) {
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
return await client.sendMessage({
|
|
523
|
+
chat_id: params.chat_id,
|
|
524
|
+
text: params.rawSource,
|
|
525
|
+
reply_markup: params.replyMarkup,
|
|
526
|
+
}, { signal: params.signal });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Best-effort: append an expiry/cancel note to the buttons message while KEEPING the
|
|
531
|
+
* inline keyboard (Telegram strips the keyboard on `editMessageText` unless reply_markup
|
|
532
|
+
* is re-sent). Renders through the same MarkdownV2-with-plain-fallback path. Uses its own
|
|
533
|
+
* bounded signal — never the aborted call signal — and swallows every failure so a failed
|
|
534
|
+
* edit can never replace the primary ask result.
|
|
535
|
+
*/
|
|
536
|
+
async function editExpiredTelegramAskMessage(client, adapter, params) {
|
|
537
|
+
try {
|
|
538
|
+
const source = `${params.rawSource}\n\n${params.marker}`;
|
|
539
|
+
const rendered = adapter.renderTelegramMarkdown(source);
|
|
540
|
+
const signal = AbortSignal.timeout(TELEGRAM_ASK_EXPIRY_EDIT_TIMEOUT_MS);
|
|
541
|
+
try {
|
|
542
|
+
await client.editMessageText({
|
|
543
|
+
chat_id: params.chat_id,
|
|
544
|
+
message_id: params.message_id,
|
|
545
|
+
text: rendered,
|
|
546
|
+
parse_mode: "MarkdownV2",
|
|
547
|
+
reply_markup: params.replyMarkup,
|
|
548
|
+
}, { signal });
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
if (!isTelegramParseError(error, adapter)) {
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
await client.editMessageText({
|
|
555
|
+
chat_id: params.chat_id,
|
|
556
|
+
message_id: params.message_id,
|
|
557
|
+
text: source,
|
|
558
|
+
reply_markup: params.replyMarkup,
|
|
559
|
+
}, { signal });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Best-effort: a failed expiry edit must never replace the ask result.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function registerTelegramAskTool(server, settings, client, adapter, fetchImpl) {
|
|
369
567
|
server.registerTool("TelegramAskButtons", {
|
|
370
568
|
title: "Ask via Telegram buttons",
|
|
371
|
-
description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard buttons (e.g. a confirmation or a multiple-choice).
|
|
569
|
+
description: "Ask the owner a question in an allowed Telegram chat with tappable inline-keyboard buttons (e.g. a confirmation or a multiple-choice). By default WAITS for their tap (up to the ask timeout) and returns the tapped label; a second concurrent ask in the same chat fails. On a proactive or scheduled turn (cron/heartbeat) set `wait:false`: the buttons are sent and the tool returns immediately, and the tap arrives later as a new message quoting the question.",
|
|
372
570
|
inputSchema: {
|
|
373
571
|
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
374
572
|
question: z.string().min(1).describe("The question to show above the buttons."),
|
|
@@ -378,32 +576,95 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
378
576
|
.max(adapter.TELEGRAM_ASK_MAX_OPTIONS)
|
|
379
577
|
.describe("Button labels (2–8). The label the user taps is echoed back as a new message."),
|
|
380
578
|
note: z.string().min(1).optional().describe("Optional extra context shown beneath the question."),
|
|
579
|
+
wait: z
|
|
580
|
+
.boolean()
|
|
581
|
+
.optional()
|
|
582
|
+
.describe("Default true: block until the user taps (up to the ask timeout). Set false on proactive/scheduled (cron/heartbeat) turns — send the buttons and return immediately; the tapped answer arrives later as a new message quoting the question."),
|
|
381
583
|
},
|
|
382
|
-
}, async (args) => {
|
|
584
|
+
}, async (args, extra) => {
|
|
383
585
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
384
586
|
const inlineKeyboard = args.options.map((label, index) => [
|
|
385
587
|
{ text: label, callback_data: adapter.telegramAskCallbackData(index) },
|
|
386
588
|
]);
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
589
|
+
const replyMarkup = { inline_keyboard: inlineKeyboard };
|
|
590
|
+
// Raw source is what the bridge records and what the plain-text fallback (and the
|
|
591
|
+
// expiry edit) re-send; the rendered form is only for the MarkdownV2 send.
|
|
592
|
+
const source = args.note === undefined ? args.question : `${args.question}\n\n${args.note}`;
|
|
593
|
+
const rendered = adapter.renderTelegramMarkdown(source);
|
|
594
|
+
// Non-blocking mode (proactive/scheduled turns): send the buttons and return.
|
|
595
|
+
// No bridge is consulted — this must work even with no bridge configured.
|
|
596
|
+
if (args.wait === false) {
|
|
597
|
+
const sent = await sendTelegramAskKeyboard(client, adapter, {
|
|
598
|
+
chat_id: args.chat_id,
|
|
599
|
+
rendered,
|
|
600
|
+
rawSource: source,
|
|
601
|
+
replyMarkup,
|
|
602
|
+
signal: extra.signal,
|
|
603
|
+
});
|
|
604
|
+
return askToolResult(`Sent the question with ${String(args.options.length)} buttons to ${String(sent.chat.id)} (message ${String(sent.message_id)}). Not waiting — the tapped answer will arrive as a new message.`, { answered: false, reason: "not_waiting" }, { chat_id: sent.chat.id, message_id: sent.message_id, options: args.options });
|
|
605
|
+
}
|
|
606
|
+
// Blocking mode requires the interaction bridge to correlate the tap.
|
|
607
|
+
const bridge = settings.askBridge;
|
|
608
|
+
if (bridge === undefined) {
|
|
609
|
+
return askToolResult("TelegramAskButtons requires a live interaction bridge, but none is configured.", {
|
|
610
|
+
answered: false,
|
|
611
|
+
reason: "unsupported_channel",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const conversationId = `telegram:${String(args.chat_id)}`;
|
|
615
|
+
const created = await askBridgeRequest(bridge, fetchImpl, "POST", "/v1/asks", {
|
|
616
|
+
conversationId,
|
|
617
|
+
producerConversationId: bridge.conversationId,
|
|
618
|
+
runId: bridge.runId,
|
|
619
|
+
toolName: "TelegramAskButtons",
|
|
620
|
+
question: source,
|
|
621
|
+
options: args.options,
|
|
622
|
+
timeoutMs: bridge.timeoutMs,
|
|
623
|
+
postQuestion: false,
|
|
624
|
+
answerKind: "callback",
|
|
625
|
+
}, extra.signal);
|
|
626
|
+
if (created.status === 409) {
|
|
627
|
+
return askToolResult("A question is already pending for this Telegram chat. Wait for its answer instead of asking again.", { answered: false, reason: "already_pending" }, { chat_id: args.chat_id });
|
|
628
|
+
}
|
|
629
|
+
if (created.status === 501) {
|
|
630
|
+
return askToolResult("This Telegram chat is not registered with the interaction bridge. Ask your question in your final reply instead.", { answered: false, reason: "unsupported_channel" }, { chat_id: args.chat_id });
|
|
631
|
+
}
|
|
632
|
+
if (created.status !== 201 || typeof created.body.askId !== "string") {
|
|
633
|
+
throw new Error(`TelegramAskButtons: the interaction bridge rejected the ask (HTTP ${String(created.status)}).`);
|
|
634
|
+
}
|
|
635
|
+
const askId = created.body.askId;
|
|
636
|
+
let result;
|
|
637
|
+
try {
|
|
638
|
+
result = await sendTelegramAskKeyboard(client, adapter, {
|
|
639
|
+
chat_id: args.chat_id,
|
|
640
|
+
rendered,
|
|
641
|
+
rawSource: source,
|
|
642
|
+
replyMarkup,
|
|
643
|
+
signal: extra.signal,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
await cleanupBridgeAskBestEffort(bridge, fetchImpl, askId);
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
const askResult = await awaitBridgeAsk(bridge, askId, "TelegramAskButtons", extra, fetchImpl, extra.signal, {
|
|
651
|
+
chat_id: result.chat.id,
|
|
652
|
+
message_id: result.message_id,
|
|
653
|
+
options: args.options,
|
|
392
654
|
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
structuredContent: {
|
|
401
|
-
ok: true,
|
|
655
|
+
// An expired/cancelled ask leaves a live-looking keyboard forever; append a note
|
|
656
|
+
// while keeping the buttons (a tap still starts a fresh turn). Best-effort only.
|
|
657
|
+
const reason = askResult.structuredContent.reason;
|
|
658
|
+
if (reason === "timeout" || reason === "cancelled") {
|
|
659
|
+
await editExpiredTelegramAskMessage(client, adapter, {
|
|
402
660
|
chat_id: result.chat.id,
|
|
403
661
|
message_id: result.message_id,
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
662
|
+
rawSource: source,
|
|
663
|
+
marker: reason === "timeout" ? TELEGRAM_ASK_EXPIRED_MARKER : TELEGRAM_ASK_CANCELLED_MARKER,
|
|
664
|
+
replyMarkup,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return askResult;
|
|
407
668
|
});
|
|
408
669
|
}
|
|
409
670
|
/**
|
|
@@ -417,7 +678,7 @@ function registerTelegramAskTool(server, settings, client, adapter) {
|
|
|
417
678
|
function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
418
679
|
server.registerTool("TelegramSendFile", {
|
|
419
680
|
title: "Send Telegram file",
|
|
420
|
-
description: "Upload and send a file to an allowed Telegram chat. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Provide the bytes as base64 `data` (with a `filename` — required for a document), or a workspace `path
|
|
681
|
+
description: "Upload and send a file to an allowed Telegram chat. Set `kind:\"document\"` to send any file (shown as a downloadable document) or `kind:\"photo\"` to send an image inline. Provide the bytes as base64 `data` (with a `filename` — required for a document), or a workspace `path`. A self-hosted Bot API can stream legacy path uploads; strict run-output paths are always read through a pinned descriptor.",
|
|
421
682
|
inputSchema: {
|
|
422
683
|
kind: z.enum(["document", "photo"]).describe("`document` for any file (downloadable), `photo` for an image shown inline."),
|
|
423
684
|
chat_id: z.union([z.string().min(1), z.number().int()]).describe("Telegram chat id from the adapter allowlist."),
|
|
@@ -426,17 +687,27 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
426
687
|
filename: z.string().min(1).optional().describe("Filename to present. Required with `data` for a document; derived from `path` otherwise."),
|
|
427
688
|
caption: z.string().min(1).optional().describe("Optional caption shown with the file."),
|
|
428
689
|
},
|
|
429
|
-
}, async (args) => {
|
|
690
|
+
}, async (args, extra) => {
|
|
691
|
+
extra.signal.throwIfAborted();
|
|
430
692
|
const kind = args.kind;
|
|
431
693
|
assertTelegramChatAllowed(settings, args.chat_id);
|
|
694
|
+
if ((args.data !== undefined) === (args.path !== undefined)) {
|
|
695
|
+
throw new Error("provide exactly one of `data` (base64) or `path`.");
|
|
696
|
+
}
|
|
432
697
|
const maxUploadBytes = settings.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
698
|
+
const strictPathUpload = args.path !== undefined && settings.sendTools?.pathScope === "run-output";
|
|
699
|
+
const strictFile = strictPathUpload
|
|
700
|
+
? await readStrictTelegramFile(settings, args.path, maxUploadBytes, extra.signal, args.filename)
|
|
701
|
+
: undefined;
|
|
702
|
+
const uploadPath = args.path === undefined || strictPathUpload
|
|
703
|
+
? undefined
|
|
704
|
+
: await resolveTelegramUploadPath(args.path);
|
|
433
705
|
// file:// fast path: a --local self-hosted server reads the file straight
|
|
434
706
|
// from disk, so a path upload needs no buffering at any size — only a
|
|
435
707
|
// stat-level cap check. Falls back once to the buffered path when the
|
|
436
708
|
// server rejects the URI (e.g. a non---local self-hosted root).
|
|
437
|
-
if (kind === "document" && settings.apiRoot !== undefined &&
|
|
438
|
-
const
|
|
439
|
-
const info = await stat(resolved);
|
|
709
|
+
if (kind === "document" && settings.apiRoot !== undefined && uploadPath !== undefined && args.data === undefined) {
|
|
710
|
+
const info = await stat(uploadPath);
|
|
440
711
|
if (!info.isFile() || info.size === 0) {
|
|
441
712
|
throw new Error("file is empty or not a regular file.");
|
|
442
713
|
}
|
|
@@ -446,10 +717,10 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
446
717
|
try {
|
|
447
718
|
const sent = await client.sendDocument({
|
|
448
719
|
chat_id: args.chat_id,
|
|
449
|
-
document: pathToFileURL(
|
|
720
|
+
document: pathToFileURL(uploadPath).href,
|
|
450
721
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
451
|
-
});
|
|
452
|
-
const name = basename(
|
|
722
|
+
}, { signal: extra.signal });
|
|
723
|
+
const name = basename(uploadPath);
|
|
453
724
|
return {
|
|
454
725
|
content: [{ type: "text", text: `Sent ${kind} ${sent.message_id} (${name}) to ${String(sent.chat.id)}.` }],
|
|
455
726
|
structuredContent: { ok: true, chat_id: sent.chat.id, message_id: sent.message_id, filename: name },
|
|
@@ -462,12 +733,14 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
462
733
|
}
|
|
463
734
|
}
|
|
464
735
|
}
|
|
465
|
-
const { bytes, filename } = await resolveTelegramFileBytes({
|
|
736
|
+
const { bytes, filename } = strictFile ?? await resolveTelegramFileBytes({
|
|
466
737
|
data: args.data,
|
|
467
738
|
path: args.path,
|
|
739
|
+
...(uploadPath === undefined ? {} : { resolvedPath: uploadPath }),
|
|
468
740
|
filename: args.filename,
|
|
469
741
|
requireFilename: kind === "document",
|
|
470
742
|
maxBytes: maxUploadBytes,
|
|
743
|
+
signal: extra.signal,
|
|
471
744
|
});
|
|
472
745
|
const result = kind === "document"
|
|
473
746
|
? await client.sendDocument({
|
|
@@ -475,13 +748,13 @@ function registerTelegramSendFileTool(server, settings, client, adapter) {
|
|
|
475
748
|
document: bytes,
|
|
476
749
|
filename,
|
|
477
750
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
478
|
-
})
|
|
751
|
+
}, { signal: extra.signal })
|
|
479
752
|
: await client.sendPhoto({
|
|
480
753
|
chat_id: args.chat_id,
|
|
481
754
|
photo: bytes,
|
|
482
755
|
filename,
|
|
483
756
|
...(args.caption === undefined ? {} : { caption: args.caption }),
|
|
484
|
-
});
|
|
757
|
+
}, { signal: extra.signal });
|
|
485
758
|
return {
|
|
486
759
|
content: [{ type: "text", text: `Sent ${kind} ${result.message_id} (${filename}) to ${String(result.chat.id)}.` }],
|
|
487
760
|
structuredContent: { ok: true, chat_id: result.chat.id, message_id: result.message_id, filename },
|
|
@@ -510,8 +783,8 @@ async function resolveTelegramFileBytes(input) {
|
|
|
510
783
|
}
|
|
511
784
|
}
|
|
512
785
|
else {
|
|
513
|
-
const resolved = resolvePath(process.cwd(), input.path);
|
|
514
|
-
bytes = new Uint8Array(await readFile(resolved));
|
|
786
|
+
const resolved = input.resolvedPath ?? resolvePath(process.cwd(), input.path);
|
|
787
|
+
bytes = new Uint8Array(await readFile(resolved, { signal: input.signal }));
|
|
515
788
|
filename = input.filename ?? basename(resolved);
|
|
516
789
|
}
|
|
517
790
|
if (bytes.byteLength === 0) {
|
|
@@ -522,6 +795,110 @@ async function resolveTelegramFileBytes(input) {
|
|
|
522
795
|
}
|
|
523
796
|
return { bytes, filename };
|
|
524
797
|
}
|
|
798
|
+
async function resolveTelegramUploadPath(inputPath) {
|
|
799
|
+
return resolvePath(process.cwd(), inputPath);
|
|
800
|
+
}
|
|
801
|
+
const STRICT_TELEGRAM_PATH_ERROR = "TelegramSendFile: path must be a regular file inside the current run output directory.";
|
|
802
|
+
/**
|
|
803
|
+
* Read a strict upload through one no-follow file descriptor. Every path check
|
|
804
|
+
* deliberately collapses to the same error so callers cannot use the tool as
|
|
805
|
+
* an existence oracle for files outside the run directory.
|
|
806
|
+
*/
|
|
807
|
+
async function readStrictTelegramFile(settings, inputPath, maxBytes, signal, requestedFilename) {
|
|
808
|
+
try {
|
|
809
|
+
signal.throwIfAborted();
|
|
810
|
+
if (settings.runOutputDir === undefined || settings.runOutputIdentity === undefined) {
|
|
811
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
812
|
+
}
|
|
813
|
+
const rootStats = await lstat(settings.runOutputDir);
|
|
814
|
+
if (!rootStats.isDirectory()
|
|
815
|
+
|| rootStats.isSymbolicLink()
|
|
816
|
+
|| !sameFileIdentity(rootStats, settings.runOutputIdentity)) {
|
|
817
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
818
|
+
}
|
|
819
|
+
const rootReal = await realpath(settings.runOutputDir);
|
|
820
|
+
const candidate = resolvePath(process.cwd(), inputPath);
|
|
821
|
+
const candidateLinkStats = await lstat(candidate);
|
|
822
|
+
if (!candidateLinkStats.isFile()
|
|
823
|
+
|| candidateLinkStats.isSymbolicLink()
|
|
824
|
+
|| candidateLinkStats.nlink !== 1) {
|
|
825
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
826
|
+
}
|
|
827
|
+
const candidateReal = await realpath(candidate);
|
|
828
|
+
if (!pathIsInside(rootReal, candidateReal))
|
|
829
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
830
|
+
const handle = await open(candidateReal, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
831
|
+
try {
|
|
832
|
+
const openedStats = await handle.stat();
|
|
833
|
+
if (!openedStats.isFile()
|
|
834
|
+
|| openedStats.nlink !== 1
|
|
835
|
+
|| openedStats.size === 0
|
|
836
|
+
|| openedStats.size > maxBytes) {
|
|
837
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
838
|
+
}
|
|
839
|
+
if (!sameFileIdentity(openedStats, fileIdentity(candidateLinkStats))) {
|
|
840
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
841
|
+
}
|
|
842
|
+
await assertPinnedCandidate(candidateReal, openedStats);
|
|
843
|
+
await assertUnchangedRunRoot(settings.runOutputDir, rootReal, settings.runOutputIdentity);
|
|
844
|
+
const bytes = await readPinnedBytes(handle, openedStats.size, signal);
|
|
845
|
+
await assertPinnedCandidate(candidateReal, openedStats);
|
|
846
|
+
await assertUnchangedRunRoot(settings.runOutputDir, rootReal, settings.runOutputIdentity);
|
|
847
|
+
const finalStats = await handle.stat();
|
|
848
|
+
if (!sameFileIdentity(finalStats, fileIdentity(openedStats))
|
|
849
|
+
|| finalStats.nlink !== 1
|
|
850
|
+
|| finalStats.size !== openedStats.size
|
|
851
|
+
|| bytes.byteLength === 0
|
|
852
|
+
|| bytes.byteLength > maxBytes) {
|
|
853
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
854
|
+
}
|
|
855
|
+
return { bytes, filename: requestedFilename ?? basename(candidateReal) };
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
await handle.close();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
if (signal.aborted)
|
|
863
|
+
throw signal.reason ?? error;
|
|
864
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function readPinnedBytes(handle, size, signal) {
|
|
868
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
869
|
+
let offset = 0;
|
|
870
|
+
while (offset < size) {
|
|
871
|
+
signal.throwIfAborted();
|
|
872
|
+
const read = await handle.read(bytes, offset, size - offset, offset);
|
|
873
|
+
if (read.bytesRead === 0)
|
|
874
|
+
break;
|
|
875
|
+
offset += read.bytesRead;
|
|
876
|
+
}
|
|
877
|
+
if (offset !== size)
|
|
878
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
879
|
+
return new Uint8Array(bytes);
|
|
880
|
+
}
|
|
881
|
+
function pathIsInside(root, candidate) {
|
|
882
|
+
const fromRoot = relative(root, candidate);
|
|
883
|
+
return fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
884
|
+
}
|
|
885
|
+
async function assertPinnedCandidate(path, opened) {
|
|
886
|
+
const current = await lstat(path);
|
|
887
|
+
if (!current.isFile()
|
|
888
|
+
|| current.isSymbolicLink()
|
|
889
|
+
|| current.nlink !== 1
|
|
890
|
+
|| !sameFileIdentity(current, fileIdentity(opened))) {
|
|
891
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function assertUnchangedRunRoot(path, expectedRealPath, expectedIdentity) {
|
|
895
|
+
const current = await lstat(path);
|
|
896
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !sameFileIdentity(current, expectedIdentity)) {
|
|
897
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
898
|
+
}
|
|
899
|
+
if (await realpath(path) !== expectedRealPath)
|
|
900
|
+
throw new Error(STRICT_TELEGRAM_PATH_ERROR);
|
|
901
|
+
}
|
|
525
902
|
async function resolveSlackSendToolSettings(input, options) {
|
|
526
903
|
try {
|
|
527
904
|
const adapter = await loadSlackModule();
|
|
@@ -542,13 +919,16 @@ async function resolveSlackSendToolSettings(input, options) {
|
|
|
542
919
|
return undefined;
|
|
543
920
|
}
|
|
544
921
|
}
|
|
545
|
-
async function resolveTelegramSendToolSettings(input, options, tools) {
|
|
922
|
+
async function resolveTelegramSendToolSettings(input, options, tools, askBridge) {
|
|
546
923
|
try {
|
|
547
924
|
const adapter = await loadTelegramModule();
|
|
548
925
|
const config = await adapter.loadTelegramAdapterConfig({ env: input.env, jsonPath: input.configPath });
|
|
549
926
|
if (!config.enabled) {
|
|
550
927
|
return undefined;
|
|
551
928
|
}
|
|
929
|
+
const producingConversationId = optionalString(input.env.MONO_AGENT_ADAPTER_TOOLS_PRODUCING_CONVERSATION_ID);
|
|
930
|
+
const runOutputDir = optionalString(input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DIR);
|
|
931
|
+
const runOutputIdentity = parseFileIdentity(input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_DEV, input.env.MONO_AGENT_ADAPTER_TOOLS_RUN_OUTPUT_INO);
|
|
552
932
|
return {
|
|
553
933
|
botToken: config.botToken,
|
|
554
934
|
allowedChatIds: config.allowedChatIds,
|
|
@@ -556,6 +936,11 @@ async function resolveTelegramSendToolSettings(input, options, tools) {
|
|
|
556
936
|
...(config.apiRoot === undefined ? {} : { apiRoot: config.apiRoot }),
|
|
557
937
|
maxUploadBytes: config.attachments?.maxUploadBytes ?? adapter.DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
558
938
|
tools,
|
|
939
|
+
...(askBridge === undefined ? {} : { askBridge }),
|
|
940
|
+
...(config.sendTools === undefined ? {} : { sendTools: config.sendTools }),
|
|
941
|
+
...(producingConversationId === undefined ? {} : { producingConversationId }),
|
|
942
|
+
...(runOutputDir === undefined ? {} : { runOutputDir }),
|
|
943
|
+
...(runOutputIdentity === undefined ? {} : { runOutputIdentity }),
|
|
559
944
|
};
|
|
560
945
|
}
|
|
561
946
|
catch (error) {
|
|
@@ -573,11 +958,81 @@ function assertSlackChannelAllowed(settings, channel) {
|
|
|
573
958
|
}
|
|
574
959
|
function assertTelegramChatAllowed(settings, chatId) {
|
|
575
960
|
const normalized = String(chatId);
|
|
961
|
+
if (settings.sendTools?.scope === "producing-conversation") {
|
|
962
|
+
const producingChatId = telegramChatIdFromConversation(settings.producingConversationId);
|
|
963
|
+
if (producingChatId === undefined) {
|
|
964
|
+
throw new Error("TelegramSendMessage: producing Telegram conversation context is unavailable.");
|
|
965
|
+
}
|
|
966
|
+
if (normalized !== producingChatId) {
|
|
967
|
+
throw new Error("TelegramSendMessage: chat_id must match the producing Telegram conversation.");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
576
970
|
if (settings.allowAllChats || settings.allowedChatIds.includes(normalized)) {
|
|
577
971
|
return;
|
|
578
972
|
}
|
|
579
973
|
throw new Error("TelegramSendMessage: chat_id is not allowed by Telegram adapter config.");
|
|
580
974
|
}
|
|
975
|
+
function telegramChatIdFromConversation(conversationId) {
|
|
976
|
+
if (conversationId === undefined)
|
|
977
|
+
return undefined;
|
|
978
|
+
const base = conversationId.split("#", 1)[0] ?? conversationId;
|
|
979
|
+
if (!base.startsWith("telegram:"))
|
|
980
|
+
return undefined;
|
|
981
|
+
const chatId = base.slice("telegram:".length);
|
|
982
|
+
return chatId.length === 0 ? undefined : chatId;
|
|
983
|
+
}
|
|
984
|
+
const SAFE_ADAPTER_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u;
|
|
985
|
+
async function ensureAdapterRunOutputDir(root, runId) {
|
|
986
|
+
if (!SAFE_ADAPTER_RUN_ID.test(runId)) {
|
|
987
|
+
throw new Error("adapter-send-tools: run id is unsafe for the run output directory.");
|
|
988
|
+
}
|
|
989
|
+
const outputRoot = resolvePath(root);
|
|
990
|
+
const runOutputDir = join(outputRoot, runId);
|
|
991
|
+
await mkdir(outputRoot, { recursive: true, mode: 0o700 });
|
|
992
|
+
try {
|
|
993
|
+
await mkdir(runOutputDir, { mode: 0o700 });
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
if (error.code !== "EEXIST")
|
|
997
|
+
throw error;
|
|
998
|
+
const existing = await lstat(runOutputDir);
|
|
999
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
1000
|
+
throw new Error("adapter-send-tools: run output path is not a real directory.");
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const current = await lstat(runOutputDir);
|
|
1004
|
+
if (!current.isDirectory() || current.isSymbolicLink()) {
|
|
1005
|
+
throw new Error("adapter-send-tools: run output path is not a real directory.");
|
|
1006
|
+
}
|
|
1007
|
+
return { path: runOutputDir, identity: fileIdentity(current) };
|
|
1008
|
+
}
|
|
1009
|
+
function fileIdentity(stats) {
|
|
1010
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
1011
|
+
}
|
|
1012
|
+
function sameFileIdentity(stats, expected) {
|
|
1013
|
+
return stats.dev === expected.dev && stats.ino === expected.ino;
|
|
1014
|
+
}
|
|
1015
|
+
function parseFileIdentity(devValue, inoValue) {
|
|
1016
|
+
const dev = Number(devValue);
|
|
1017
|
+
const ino = Number(inoValue);
|
|
1018
|
+
return Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino > 0
|
|
1019
|
+
? { dev, ino }
|
|
1020
|
+
: undefined;
|
|
1021
|
+
}
|
|
1022
|
+
async function removeOwnedDirectory(path, expected) {
|
|
1023
|
+
let current;
|
|
1024
|
+
try {
|
|
1025
|
+
current = await lstat(path);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
if (error.code === "ENOENT")
|
|
1029
|
+
return;
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !sameFileIdentity(current, expected))
|
|
1033
|
+
return;
|
|
1034
|
+
await rm(path, { recursive: true, force: true });
|
|
1035
|
+
}
|
|
581
1036
|
function optionalString(value) {
|
|
582
1037
|
if (value === undefined) {
|
|
583
1038
|
return undefined;
|