@segmentstream/agent 0.6.11 → 0.6.13

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.
Files changed (35) hide show
  1. package/README.md +10 -8
  2. package/dist/_chunks/{inbound-request-DRNPIIpb.mjs → inbound-request-CysnjMdw.mjs} +62 -62
  3. package/dist/extension/config.d.ts +7 -8
  4. package/dist/extension/config.mjs +20 -2
  5. package/dist/extension/instructions.md +8 -1
  6. package/dist/extension/lib/chat-post-confirmation.mjs +1 -1
  7. package/dist/extension/lib/confirmation-description.d.ts +1 -0
  8. package/dist/extension/lib/confirmation-description.mjs +29 -13
  9. package/dist/extension/lib/confirmations.d.ts +3 -0
  10. package/dist/extension/lib/confirmations.mjs +22 -5
  11. package/dist/extension/lib/customer-text-safety.d.ts +1 -0
  12. package/dist/extension/lib/customer-text-safety.mjs +23 -0
  13. package/dist/extension/lib/ontology-tools.mjs +13 -6
  14. package/dist/extension/lib/telegram/adapter.mjs +10 -6
  15. package/dist/extension/lib/telegram/attachments-send.mjs +6 -6
  16. package/dist/extension/lib/telegram/chat-post-card.d.ts +6 -13
  17. package/dist/extension/lib/telegram/chat-post-card.mjs +29 -37
  18. package/dist/extension/lib/telegram/confirmation-text.d.ts +20 -8
  19. package/dist/extension/lib/telegram/confirmation-text.mjs +55 -65
  20. package/dist/extension/lib/telegram/confirmations.d.ts +1 -2
  21. package/dist/extension/lib/telegram/confirmations.mjs +40 -96
  22. package/dist/extension/lib/telegram/content.d.ts +1 -1
  23. package/dist/extension/lib/telegram/conversations.mjs +1 -1
  24. package/dist/extension/lib/telegram/customer-text.d.ts +5 -5
  25. package/dist/extension/lib/telegram/customer-text.mjs +2 -1
  26. package/dist/extension/lib/telegram/files.mjs +1 -1
  27. package/dist/extension/lib/telegram/images.mjs +1 -1
  28. package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
  29. package/dist/extension/lib/telegram/inbound.mjs +5 -5
  30. package/dist/extension/lib/telegram/media-policy.mjs +1 -1
  31. package/dist/extension/lib/telegram/principal.mjs +1 -1
  32. package/dist/extension/lib/telegram/silent.mjs +2 -2
  33. package/dist/extension/lib/telegram/turn-errors.mjs +2 -2
  34. package/dist/extension/lib/telegram/voice.mjs +1 -1
  35. package/package.json +2 -1
package/README.md CHANGED
@@ -79,14 +79,16 @@ Both entry points export the same handle: `@segmentstream/agent` and
79
79
  }
80
80
  ```
81
81
 
82
- | Field | Default | Meaning |
83
- | ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
84
- | `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
85
- | `channels.telegram.policy` | `"smart"` | Unaddressed group text: `smart` asks the decision model, `mention` observes; attachments always trigger a turn |
86
- | `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
87
- | `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
88
- | `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
89
- | `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
82
+ | Field | Default | Meaning |
83
+ | ---------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
85
+ | `channels.telegram.policy` | `"smart"` | Unaddressed group text: `smart` asks the decision model, `mention` observes; attachments always trigger a turn |
86
+ | `channels.telegram.linkPreviews` | `all` | Keep native first-link previews; opt into `external` to preview external links only, or `none` to disable previews. Caller options take precedence. |
87
+ | `channels.telegram.additionalOwnOrigins` | `[]` | Additional exact HTTP(S) workspace origins, including local ports; no paths or credentials. |
88
+ | `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
89
+ | `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
90
+ | `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
91
+ | `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
90
92
 
91
93
  An unknown field or an invalid value fails the mount with `SegmentStreamAgentConfigError`, naming
92
94
  each path. `manifestOf(config)` returns the normalized configuration, defaults applied, for the
@@ -4,10 +4,13 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { isExactChatPostCard } from "../extension/lib/telegram/chat-post-card.mjs";
7
+ import { isTechnicalText } from "../extension/lib/customer-text-safety.mjs";
8
+ import { isConfirmationText } from "../extension/lib/telegram/confirmation-text.mjs";
9
+ import { chatPostCardLinks, isExactChatPostCard } from "../extension/lib/telegram/chat-post-card.mjs";
8
10
  import { redactDiagnostic } from "../extension/lib/diagnostics.mjs";
9
11
  import { plainTelegramText, splitTelegramText, telegramMarkdown } from "../extension/lib/telegram/format.mjs";
10
12
  import { inTelegramSendQueue } from "../extension/lib/telegram/send-queue.mjs";
13
+ import { telegramPreviewOptions, telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
11
14
  import { randomBytes } from "node:crypto";
12
15
  import { AsyncLocalStorage } from "node:async_hooks";
13
16
  const deliveryLocale = new AsyncLocalStorage();
@@ -23,43 +26,17 @@ function currentLocale() {
23
26
  function currentThreadId() {
24
27
  return deliveryLocale.getStore()?.threadId ?? currentInboundRequest()?.threadId;
25
28
  }
26
- function customerCopy(locale = currentLocale()) {
27
- return locale.toLowerCase().split(/[-_]/)[0] === "ru" ? {
28
- failed: "Не удалось выполнить запрос. Попробуйте ещё раз.",
29
- recovery: "Не удалось продолжить разговор. Отправьте новое сообщение.",
30
- attachment: "Не удалось обработать файл. Попробуйте ещё раз.",
31
- authorization: "Не удалось подключиться. Попробуйте ещё раз.",
32
- response: "Не удалось показать ответ. Попробуйте ещё раз.",
33
- reference: "Код ошибки",
34
- connected: "Доступ подключён.",
35
- disconnected: "Подключение отменено."
36
- } : {
37
- failed: "I couldn't complete the request. Please try again.",
38
- recovery: "I couldn't continue the conversation. Please send a new message.",
39
- attachment: "I couldn't process the file. Please try again.",
40
- authorization: "I couldn't connect. Please try again.",
41
- response: "I couldn't display the response. Please try again.",
42
- reference: "Error reference",
43
- connected: "Access connected.",
44
- disconnected: "Connection cancelled."
29
+ function customerCopy() {
30
+ return {
31
+ failed: "⚠️",
32
+ recovery: "⚠️",
33
+ attachment: "⚠️",
34
+ authorization: "⚠️",
35
+ response: "⚠️",
36
+ connected: "✅",
37
+ disconnected: "❌"
45
38
  };
46
39
  }
47
- const TECHNICAL_PATTERNS = [
48
- /[{}]|```/u,
49
- /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu,
50
- /\b[0-9a-hjkmnp-tv-z]{26}\b/iu,
51
- /\b\w+__\w+/u,
52
- /\b(?:Serialization|Type|Reference|Syntax|Tool|Provider|API|Workflow|InternalServer)Error\b/u,
53
- /\bError:\s|(?:^|\n)\s*at\s+\S+|\b(?:stack trace|traceback|SQLSTATE|node_modules)\b/imu,
54
- /\b(?:duplicate key|ECONN\w+|HTTP [45]\d\d)\b/iu,
55
- /\[\s*(?:"[^"]*"|\d+|true|false|null)\s*[,\]]/u
56
- ];
57
- function isTechnicalText(text) {
58
- if (/https?:\/\/[^\s/@]+:[^\s/@]+@|[?&](?:token|access_token|api_key|secret|password)=/iu.test(text)) return true;
59
- const visible = text.replace(/https?:\/\/[^\s)<>]+/giu, "link");
60
- const prose = visible.replace(/(?<![\w@])@[a-z][a-z0-9_]{4,31}(?!\w)/giu, "recipient");
61
- return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible)) || /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu.test(prose);
62
- }
63
40
  function redactCredentials(value) {
64
41
  if (typeof value === "string") return value.replace(/\bBearer [A-Za-z0-9_.~+/-]+=*/giu, "Bearer [redacted]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gu, "$1[redacted]@").replace(/([?&](?:token|access_token|api_key|secret|password)=)[^&#\s]*/giu, "$1[redacted]").replace(/("(?:token|api_key|password|secret)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, "$1\"[redacted]\"");
65
42
  if (Array.isArray(value)) return value.map(redactCredentials);
@@ -77,16 +54,18 @@ function reportTelegramError(operation, error, metadata = {}, secrets = new Set(
77
54
  });
78
55
  return errorId;
79
56
  }
80
- function failureText(errorId, kind = "failed", locale = currentLocale()) {
57
+ function failureText(errorId, kind = "failed", _locale = currentLocale()) {
81
58
  if (kind === "cancelled") return `⏹ ${errorId}`;
82
- const copy = customerCopy(locale);
83
- return `${copy[kind]} ${copy.reference}: ${errorId}.`;
59
+ return `${customerCopy()[kind]} ${errorId}`;
84
60
  }
85
61
  function customerText(text, locale = currentLocale()) {
86
62
  if (!isTechnicalText(text)) return text;
87
63
  return failureText(reportTelegramError("technical_text_blocked", { text }), "response", locale);
88
64
  }
89
65
  function customerMessage(text, locale = currentLocale()) {
66
+ return guardedCustomerMessage(text, locale);
67
+ }
68
+ function guardedCustomerMessage(text, locale, blocked) {
90
69
  let message;
91
70
  try {
92
71
  message = telegramMarkdown(text);
@@ -99,14 +78,20 @@ function customerMessage(text, locale = currentLocale()) {
99
78
  }
100
79
  const links = message.entities.flatMap((entity) => entity.url === void 0 ? [] : [entity.url]);
101
80
  const visible = customerText(message.text, locale);
102
- if (visible !== message.text) return {
103
- text: visible,
104
- entities: []
105
- };
106
- if (links.some(isTechnicalText)) return {
107
- text: failureText(reportTelegramError("unsafe_markdown_link", new Error("Credential-bearing link refused.")), "response", locale),
108
- entities: []
109
- };
81
+ if (visible !== message.text) {
82
+ blocked?.();
83
+ return {
84
+ text: visible,
85
+ entities: []
86
+ };
87
+ }
88
+ if (links.some(isTechnicalText)) {
89
+ blocked?.();
90
+ return {
91
+ text: failureText(reportTelegramError("unsafe_markdown_link", new Error("Credential-bearing link refused.")), "response", locale),
92
+ entities: []
93
+ };
94
+ }
110
95
  return message;
111
96
  }
112
97
  async function formattingRefused(response) {
@@ -126,7 +111,7 @@ var TelegramDeliveryError = class extends Error {
126
111
  this.messageIds = messageIds;
127
112
  }
128
113
  };
129
- function safeTelegramFetch(fetchImpl, scope = "telegram") {
114
+ function safeTelegramFetch(fetchImpl, scope = "telegram", previews = {}) {
130
115
  const methods = new Set([
131
116
  "sendMessage",
132
117
  "editMessageText",
@@ -140,17 +125,22 @@ function safeTelegramFetch(fetchImpl, scope = "telegram") {
140
125
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
141
126
  if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
142
127
  const body = JSON.parse(init.body);
128
+ let textBlocked = false;
143
129
  for (const field of ["text", "caption"]) {
144
130
  if (typeof body[field] === "string" && isExactChatPostCard(body[field])) body.parse_mode = "MarkdownV2";
145
131
  if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) {
146
132
  const entityField = field === "text" ? "entities" : "caption_entities";
147
133
  const existing = body[entityField];
148
- const message = Array.isArray(existing) ? {
134
+ const message = Array.isArray(existing) || isConfirmationText(body[field]) ? {
149
135
  text: body[field],
150
- entities: existing
151
- } : customerMessage(body[field]);
136
+ entities: existing ?? []
137
+ } : guardedCustomerMessage(body[field], currentLocale(), () => {
138
+ textBlocked = true;
139
+ });
152
140
  const guarded = customerText(message.text);
153
- body[field] = message.entities.some((entity) => typeof entity.url === "string" && isTechnicalText(entity.url)) ? failureText(reportTelegramError("unsafe_entity_link", new Error("Credential-bearing link refused.")), "response") : guarded;
141
+ const unsafeLink = message.entities.some((entity) => typeof entity.url === "string" && isTechnicalText(entity.url));
142
+ textBlocked ||= guarded !== message.text || unsafeLink;
143
+ body[field] = unsafeLink ? failureText(reportTelegramError("unsafe_entity_link", new Error("Credential-bearing link refused.")), "response") : guarded;
154
144
  if (!url.endsWith("/answerCallbackQuery")) body[entityField] = body[field] === message.text ? message.entities : [];
155
145
  delete body.parse_mode;
156
146
  }
@@ -160,14 +150,17 @@ function safeTelegramFetch(fetchImpl, scope = "telegram") {
160
150
  ...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
161
151
  }));
162
152
  const markup = body.reply_markup;
163
- if (Array.isArray(markup?.inline_keyboard)) markup.inline_keyboard = markup.inline_keyboard.map((row) => row.map((button) => {
164
- if (typeof button.text !== "string" || !isTechnicalText(button.text)) return button;
165
- reportTelegramError("technical_button_label_blocked", { text: button.text });
166
- return {
167
- ...button,
168
- text: currentLocale().startsWith("ru") ? "Продолжить" : "Continue"
169
- };
170
- }));
153
+ if (Array.isArray(markup?.inline_keyboard)) {
154
+ if ((textBlocked || typeof body.text === "string" && body.text.length > 4096) && markup.inline_keyboard.some((row) => row.some((button) => typeof button.callback_data === "string" && button.callback_data.startsWith("ss-confirm:")))) throw new Error("Approval buttons require the complete, safe confirmation text.");
155
+ markup.inline_keyboard = markup.inline_keyboard.map((row) => row.map((button) => {
156
+ if (typeof button.text !== "string" || !isTechnicalText(button.text)) return button;
157
+ reportTelegramError("technical_button_label_blocked", { text: button.text });
158
+ return {
159
+ ...button,
160
+ text: "➡️"
161
+ };
162
+ }));
163
+ }
171
164
  const send = async () => {
172
165
  const parts = url.endsWith("/sendMessage") && typeof body.text === "string" && !isExactChatPostCard(body.text) ? splitTelegramText({
173
166
  text: body.text,
@@ -177,9 +170,16 @@ function safeTelegramFetch(fetchImpl, scope = "telegram") {
177
170
  const messageIds = [];
178
171
  const attempt = async (outgoing) => {
179
172
  try {
173
+ const previewOptions = (url.endsWith("/sendMessage") || url.endsWith("/editMessageText")) && typeof outgoing.text === "string" ? telegramPreviewOptions(body, chatPostCardLinks(outgoing.text) ?? telegramTextLinks(outgoing.text, outgoing.entities ?? []), previews) : {};
174
+ const payload = { ...outgoing };
175
+ if (url.endsWith("/sendMessage") || url.endsWith("/editMessageText")) {
176
+ delete payload.link_preview_options;
177
+ delete payload.disable_web_page_preview;
178
+ Object.assign(payload, previewOptions);
179
+ }
180
180
  return await fetchImpl(input, {
181
181
  ...init,
182
- body: JSON.stringify(outgoing)
182
+ body: JSON.stringify(payload)
183
183
  });
184
184
  } catch (error) {
185
185
  throw new TelegramDeliveryError(reportTelegramError("telegram_send_unknown", error, { messageIds }), "unknown", messageIds);
@@ -322,4 +322,4 @@ function withReplacedText(source, request) {
322
322
  }
323
323
  };
324
324
  }
325
- export { TelegramDeliveryError as a, customerCopy as c, failureText as d, formattingRefused as f, withDeliveryLocale as g, safeTelegramFetch as h, withReplacedText as i, customerMessage as l, reportTelegramError as m, currentInboundRequest as n, currentLocale as o, isTechnicalText as p, runInboundRequest as r, currentThreadId as s, InboundRequest as t, customerText as u };
325
+ export { TelegramDeliveryError as a, customerCopy as c, failureText as d, formattingRefused as f, withDeliveryLocale as h, withReplacedText as i, customerMessage as l, safeTelegramFetch as m, currentInboundRequest as n, currentLocale as o, reportTelegramError as p, runInboundRequest as r, currentThreadId as s, InboundRequest as t, customerText as u };
@@ -1,11 +1,4 @@
1
- /**
2
- * The extension's configuration: `agent/segmentstream.json`, mounted by
3
- * `agent/agent/extensions/segmentstream.ts` and read by the platform at deploy.
4
- *
5
- * eve validates a mount's configuration with a Standard Schema. The schema below is written by
6
- * hand, so the package adds no second schema library next to the Effect Schema the rest of the
7
- * repository uses.
8
- */
1
+ import { type TelegramLinkPreviews } from "@segmentstream-cloud/ontology/telegram";
9
2
  export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
10
3
  /**
11
4
  * How the Telegram channel treats group messages that do not address the bot: `smart` asks the
@@ -28,6 +21,10 @@ export interface SegmentStreamAgentConfigInput {
28
21
  readonly connection: string;
29
22
  /** `smart` (default) or `mention`. */
30
23
  readonly policy?: string;
24
+ /** Telegram’s native first link (default), external links only, or no previews. */
25
+ readonly linkPreviews?: string;
26
+ /** Additional exact HTTP(S) origins belonging to this workspace. */
27
+ readonly additionalOwnOrigins?: readonly string[];
31
28
  };
32
29
  };
33
30
  readonly models?: {
@@ -72,6 +69,8 @@ export interface SegmentStreamAgentConfigInput {
72
69
  export interface TelegramSettings {
73
70
  readonly connection: string;
74
71
  readonly policy: TelegramPolicy;
72
+ readonly linkPreviews?: TelegramLinkPreviews;
73
+ readonly additionalOwnOrigins?: readonly string[];
75
74
  }
76
75
  /** The chat models `segmentstream.model()` chooses between, as AI Gateway model ids. */
77
76
  export interface ChatModelChoice {
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
+ import { TELEGRAM_LINK_PREVIEWS, isHttpOrigin } from "@segmentstream-cloud/ontology/telegram";
7
8
  const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
8
9
  const TELEGRAM_POLICIES = ["mention", "smart"];
9
10
  const DEFAULT_STT_MODEL = "spacexai/grok-stt";
@@ -44,7 +45,12 @@ function telegramSettings(value, issues) {
44
45
  const telegram = section({
45
46
  value,
46
47
  path,
47
- allowed: ["connection", "policy"],
48
+ allowed: [
49
+ "connection",
50
+ "policy",
51
+ "linkPreviews",
52
+ "additionalOwnOrigins"
53
+ ],
48
54
  issues
49
55
  });
50
56
  if (telegram === void 0) return null;
@@ -58,9 +64,21 @@ function telegramSettings(value, issues) {
58
64
  message: "must be \"smart\" or \"mention\"",
59
65
  path: [...path, "policy"]
60
66
  });
67
+ const linkPreviews = telegram.linkPreviews === void 0 ? "all" : telegram.linkPreviews;
68
+ const additionalOwnOrigins = telegram.additionalOwnOrigins === void 0 ? [] : telegram.additionalOwnOrigins;
69
+ if (!TELEGRAM_LINK_PREVIEWS.some((mode) => mode === linkPreviews)) issues.push({
70
+ message: "must be \"external\", \"none\" or \"all\"",
71
+ path: [...path, "linkPreviews"]
72
+ });
73
+ if (!Array.isArray(additionalOwnOrigins) || !additionalOwnOrigins.every(isHttpOrigin)) issues.push({
74
+ message: "must be an array of HTTP(S) origins without paths, credentials, queries or fragments",
75
+ path: [...path, "additionalOwnOrigins"]
76
+ });
61
77
  return typeof connection === "string" && isTelegramPolicy(policy) ? {
62
78
  connection,
63
- policy
79
+ policy,
80
+ linkPreviews,
81
+ additionalOwnOrigins
64
82
  } : null;
65
83
  }
66
84
  function modelId(input) {
@@ -26,6 +26,13 @@ talking to you, and you act with exactly their permissions.
26
26
  - Destructive or irreversible actions, and legacy custom actions without safety labels, require
27
27
  the tool boundary's Confirm / Cancel buttons. Call the action to start that flow; never ask for
28
28
  approval in prose or treat a text acknowledgement as approval. Wait for the button outcome.
29
+ When the tool schema offers `_confirmation`, supply {question, confirm, cancel, done,
30
+ cancelled, failed, expired?, edit?} in the chat's language. Describe the action and key
31
+ parameters in a plain single-line question (at most 300 characters); labels are at most
32
+ 24 characters. No IDs, tool names, snake_case, URLs, credentials, or markup. The platform
33
+ adds trusted facts and symbols. Omit this argument on older tools without schema support.
34
+ Leave decision symbols to the platform and use distinct confirm/cancel labels. If trusted
35
+ target titles are unavailable or the facts are too long, resolve the targets or narrow the call.
29
36
  Cancel ends that call; retry it only after a new instruction from the member.
30
37
  - Every change goes through an action. Report results after success; never claim a record changed
31
38
  unless an action reported it.
@@ -36,7 +43,7 @@ talking to you, and you act with exactly their permissions.
36
43
  Read `chat_history` or `chat_search` only when permitted, then compose the exact draft and call
37
44
  `chat_post_message`. The runtime supplies the operation key. Its Confirm / Edit / Cancel card
38
45
  shows the full attributed text in the requesting chat; only the requester can act on it and it
39
- expires after ten minutes. Text such as "yes" or "да" cannot approve a post. After Edit, ask what
46
+ expires after ten minutes. A text acknowledgement cannot approve a post. After Edit, ask what
40
47
  to change and make a new call/card for the revision; the cancelled call cannot be reused.
41
48
  An uncertain delivery must never be automatically resent, including through a fresh call/key.
42
49
  For a reply in the current chat, answer normally. If chat posting is unavailable, say so.
@@ -48,7 +48,7 @@ function describeChatPost(input) {
48
48
  ].every((value) => typeof value === "string" && value.length > 0) || target.provider !== "telegram" || target.id !== values.chat && target.title !== values.chat) throw new Error("An unambiguous destination and a complete exact chat preview are required.");
49
49
  const footer = escapeMarkdownV2(caller.attribution);
50
50
  return {
51
- action: "Post message",
51
+ action: "❓",
52
52
  fields: [],
53
53
  chatPost: {
54
54
  chatId: target.id,
@@ -9,6 +9,7 @@ export interface ConfirmationDescription {
9
9
  readonly value: string;
10
10
  }[];
11
11
  readonly chatPost?: ChatPostPreview;
12
+ readonly warning?: boolean;
12
13
  }
13
14
  export declare function object(value: unknown): Record<string, unknown>;
14
15
  export declare function humanize(value: string): string;
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
+ import { isTechnicalText } from "./customer-text-safety.mjs";
7
8
  function object(value) {
8
9
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
9
10
  }
@@ -12,9 +13,20 @@ function humanize(value) {
12
13
  }
13
14
  function humanText(value, maximum = 180) {
14
15
  if (typeof value !== "string") return;
15
- const text = value.replace(/\s+/g, " ").trim();
16
- if (text === "" || /[{}\[\]]|__|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|\b[0-9A-HJKMNP-TV-Z]{26}\b|\b[a-z0-9]{24,}\b/iu.test(text)) return;
17
- return text.slice(0, maximum);
16
+ const text = value.trim();
17
+ if (text === "" || text.length > maximum || isTechnicalText(text) || [
18
+ /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>`*_~|\\\[\]{}]|^(?:#{1,6}|[-+])\s/u,
19
+ /[a-z][a-z0-9+.-]*:\/\/|www\.|\b[a-z][a-z0-9]*\.[a-z][a-z_0-9]*\b/iu,
20
+ /&(?:#\d+|#x[0-9a-f]+|[a-z]+);/iu,
21
+ /\bBearer\s+|\b(?:password|secret|token|credential|api[ _-]?key)\s*[:=]/iu,
22
+ /\b(?:sk|ssw|ghp)[_-][a-z0-9]+/iu,
23
+ /\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|\b[0-9A-HJKMNP-TV-Z]{26}\b|\b[a-z0-9]{24,}\b/iu
24
+ ].some((pattern) => pattern.test(text))) return;
25
+ return text;
26
+ }
27
+ function factText(value, maximum = 180) {
28
+ if (typeof value === "string" && value.trim().length > maximum) throw new Error("The complete confirmation facts cannot fit in one Telegram message.");
29
+ return humanText(value, maximum);
18
30
  }
19
31
  function fieldSchema(value, root, depth = 0) {
20
32
  const schema = object(value);
@@ -33,46 +45,50 @@ async function describeConfirmation(input) {
33
45
  const properties = Array.isArray(entity.properties) ? entity.properties.map(object) : [];
34
46
  const fields = [];
35
47
  const read = new Map();
36
- for (const [key, value] of Object.entries(input.values).slice(0, 20)) {
48
+ for (const [key, value] of Object.entries(input.values)) {
37
49
  const declared = object(input.schema.properties)[key];
38
- if (declared === void 0 || /password|secret|token|credential|authorization/i.test(key)) continue;
50
+ if (declared === void 0 || /password|secret|token|credential|authorization|api.?key|private.?key|cookie/i.test(key)) continue;
39
51
  const schema = fieldSchema(declared, input.schema);
40
52
  const itemSchema = schema.type === "array" ? fieldSchema(schema.items, input.schema) : schema;
41
53
  const property = properties.find((candidate) => candidate.name === key);
42
54
  const reference = itemSchema["x-ontology-ref"];
43
55
  const target = input.entities.find((candidate) => candidate.name === reference);
44
- const label = humanText(schema.title) ?? humanText(property?.label) ?? humanText(humanize(key));
45
- if (label === void 0 || schema.writeOnly === true || schema.format === "password") continue;
56
+ if (schema.writeOnly === true || schema.format === "password" || itemSchema.writeOnly === true || itemSchema.format === "password") continue;
57
+ const label = factText(schema.title) ?? factText(property?.label) ?? factText(humanize(key));
58
+ if (label === void 0) throw new Error("A confirmation field has no safe label.");
46
59
  const options = Array.isArray(property?.options) ? property.options.map(object) : [];
47
60
  const choices = [itemSchema.oneOf, itemSchema.anyOf].flatMap((items) => Array.isArray(items) ? items.map(object) : []);
48
61
  const values = [];
49
- for (const item of (Array.isArray(value) ? value : [value]).slice(0, 5)) {
62
+ for (const item of Array.isArray(value) ? value : [value]) {
50
63
  if (typeof reference === "string") {
64
+ if (item === null || item === void 0) continue;
51
65
  let title;
52
66
  if (target !== void 0 && typeof item === "string" && typeof target.title === "string") {
53
67
  const cacheKey = `${reference}:${item}`;
54
68
  if (!read.has(cacheKey)) read.set(cacheKey, input.read(reference, item));
55
69
  const value = (await read.get(cacheKey))?.[target.title];
56
- title = value === item ? void 0 : humanText(value);
70
+ title = value === item ? void 0 : factText(value, 3584);
57
71
  }
58
- values.push(title ?? humanText(target?.label) ?? label);
72
+ if (title === void 0) throw new Error("Every confirmation target requires an authorized, safe record title.");
73
+ values.push(title);
59
74
  continue;
60
75
  }
61
76
  if (/^(id|.*_id)$/i.test(key) || itemSchema.format === "uuid" || typeof item === "object") continue;
62
77
  const option = options.find((candidate) => candidate.value === item);
63
78
  const choice = choices.find((candidate) => candidate.const === item);
64
79
  const enumeration = Array.isArray(itemSchema.enum) || choices.length > 0 || options.length > 0;
65
- const text = humanText(option?.label) ?? humanText(choice?.title) ?? (enumeration ? void 0 : typeof item === "string" ? humanText(humanize(item)) : typeof item === "number" || typeof item === "boolean" ? String(item) : void 0);
80
+ const text = factText(option?.label) ?? factText(choice?.title) ?? (enumeration ? void 0 : typeof item === "string" ? factText(item, 3584) : typeof item === "number" || typeof item === "boolean" ? String(item) : void 0);
66
81
  if (text !== void 0) values.push(text);
67
82
  }
68
83
  if (values.length > 0) fields.push({
69
84
  label,
70
85
  value: values.join(", ")
71
86
  });
72
- if (fields.length === 6) break;
73
87
  }
88
+ const action = factText(input.title);
89
+ if (action === void 0) throw new Error("A confirmation requires a safe declared action title.");
74
90
  return {
75
- action: humanText(input.title) ?? "the requested action",
91
+ action,
76
92
  ...humanText(entity.label) === void 0 ? {} : { entity: humanText(entity.label) },
77
93
  ...humanText(input.description?.split("\n\n")[0]) === void 0 ? {} : { description: humanText(input.description?.split("\n\n")[0]) },
78
94
  fields
@@ -1,9 +1,11 @@
1
1
  import { type StateHandle } from "eve/context";
2
2
  import type { ApprovalConfiguration } from "eve/tools/approval";
3
3
  import type { ConfirmationDescription } from "./confirmation-description.js";
4
+ import { type ConfirmationCopy } from "./telegram/confirmation-text.js";
4
5
  export declare const CONFIRMATION_TTL_MS: number;
5
6
  export interface ActionAnnotations {
6
7
  readonly readOnlyHint?: boolean;
8
+ readonly confirmation?: boolean;
7
9
  readonly destructiveHint?: boolean;
8
10
  readonly irreversibleHint?: boolean;
9
11
  readonly idempotentHint?: boolean;
@@ -19,6 +21,7 @@ export interface Confirmation {
19
21
  readonly expiresAt: number;
20
22
  readonly status: "pending" | "approved" | "consumed" | "cancelled";
21
23
  readonly description?: ConfirmationDescription;
24
+ readonly copy?: ConfirmationCopy;
22
25
  readonly source?: {
23
26
  readonly chatId: string;
24
27
  readonly threadId: string;
@@ -6,6 +6,7 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { memberIdOf } from "./telegram/principal.mjs";
8
8
  import { chatPostDigest, isChatPost } from "./chat-post-confirmation.mjs";
9
+ import { confirmationCopy, renderConfirmation } from "./telegram/confirmation-text.mjs";
9
10
  import { chatPostCard } from "./telegram/chat-post-card.mjs";
10
11
  import { createHash } from "node:crypto";
11
12
  import { defineState } from "eve/context";
@@ -49,15 +50,19 @@ function actionApproval(options) {
49
50
  console.warn("The ontology action permission check failed.", error);
50
51
  return denied("The action is unavailable or the member no longer has permission.");
51
52
  }
53
+ const copy = labels.confirmation === false ? void 0 : confirmationCopy(ctx.toolInput?._confirmation);
54
+ const businessInput = Object.fromEntries(Object.entries(ctx.toolInput ?? {}).filter(([key]) => key !== "_confirmation"));
52
55
  const posting = isChatPost(ctx.toolName);
53
56
  let description;
54
57
  if (posting) try {
55
- description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
58
+ description = await options.describe?.(memberId, ctx.toolName, businessInput);
56
59
  const preview = description?.chatPost;
57
60
  const source = ctx.session.auth.current?.attributes;
58
- if (preview === void 0 || typeof source?.chatId !== "string" || source.chatId === preview.externalId) return denied("A complete preview for a different chat is required. Reply normally in this chat.");
59
- chatPostCard(preview, "en");
60
- chatPostCard(preview, "ru");
61
+ if (description === void 0 || preview === void 0 || typeof source?.chatId !== "string" || source.chatId === preview.externalId) return denied("A complete preview for a different chat is required. Reply normally in this chat.");
62
+ chatPostCard({
63
+ ...description,
64
+ warning: labels.destructiveHint === true || labels.irreversibleHint === true
65
+ }, copy);
61
66
  if (saved !== void 0 && (saved.description?.chatPost === void 0 || chatPostDigest(saved.description.chatPost) !== chatPostDigest(preview) || saved.source?.chatId !== source.chatId || saved.source.threadId !== String(source.messageThreadId ?? ""))) return denied("The destination or attribution changed. Make a new call and confirmation.");
62
67
  } catch (error) {
63
68
  console.warn("The exact chat confirmation could not be prepared.", error);
@@ -76,9 +81,20 @@ function actionApproval(options) {
76
81
  if (saved !== void 0) return denied("The original call is waiting for its Confirm / Cancel buttons.");
77
82
  if (!posting && !needsConfirmation(labels)) return "approved";
78
83
  try {
79
- if (!posting) description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
84
+ if (!posting) description = await options.describe?.(memberId, ctx.toolName, businessInput);
85
+ if (description === void 0) throw new Error("A trusted confirmation description is required.");
80
86
  } catch (error) {
81
87
  console.warn("The confirmation description could not be read.", error);
88
+ return denied("Trusted confirmation facts are incomplete, unsafe or too long. Resolve every target title and narrow the input before making a new call.");
89
+ }
90
+ description = description === void 0 ? void 0 : {
91
+ ...description,
92
+ warning: labels.destructiveHint === true || labels.irreversibleHint === true
93
+ };
94
+ try {
95
+ if (!posting) renderConfirmation(description, copy);
96
+ } catch {
97
+ return denied("The complete confirmation facts are unsafe or too long. Narrow the action input.");
82
98
  }
83
99
  state.update((all) => ({
84
100
  ...all,
@@ -88,6 +104,7 @@ function actionApproval(options) {
88
104
  inputHash: hash,
89
105
  expiresAt: now() + CONFIRMATION_TTL_MS,
90
106
  status: "pending",
107
+ ...copy === void 0 ? {} : { copy },
91
108
  ...description === void 0 ? {} : { description },
92
109
  ...posting ? { source: {
93
110
  chatId: String(ctx.session.auth.current?.attributes.chatId),
@@ -0,0 +1 @@
1
+ export declare function isTechnicalText(text: string): boolean;
@@ -0,0 +1,23 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ const TECHNICAL_PATTERNS = [
8
+ /[{}]|```/u,
9
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu,
10
+ /\b[0-9a-hjkmnp-tv-z]{26}\b/iu,
11
+ /\b\w+__\w+/u,
12
+ /\b(?:Serialization|Type|Reference|Syntax|Tool|Provider|API|Workflow|InternalServer)Error\b/u,
13
+ /\bError:\s|(?:^|\n)\s*at\s+\S+|\b(?:stack trace|traceback|SQLSTATE|node_modules)\b/imu,
14
+ /\b(?:duplicate key|ECONN\w+|HTTP [45]\d\d)\b/iu,
15
+ /\[\s*(?:"[^"]*"|\d+|true|false|null)\s*[,\]]/u
16
+ ];
17
+ function isTechnicalText(text) {
18
+ if (/https?:\/\/[^\s/@]+:[^\s/@]+@|[?&](?:token|access_token|api_key|secret|password)=/iu.test(text)) return true;
19
+ const visible = text.replace(/https?:\/\/[^\s)<>]+/giu, "link");
20
+ const prose = visible.replace(/(?<![\w@])@[a-z][a-z0-9_]{4,31}(?!\w)/giu, "recipient");
21
+ return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible)) || /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu.test(prose);
22
+ }
23
+ export { isTechnicalText };
@@ -59,7 +59,11 @@ function ontologyTools(options = {}) {
59
59
  return Array.isArray(result.tools) && result.tools.map(object).some(supportsChatPost);
60
60
  },
61
61
  async labels(memberId, action) {
62
- return object((await toolFor(memberId, action)).tool.annotations);
62
+ const { tool } = await toolFor(memberId, action);
63
+ return {
64
+ ...object(tool.annotations),
65
+ confirmation: object(object(tool.inputSchema).properties)._confirmation !== void 0
66
+ };
63
67
  },
64
68
  async describe(memberId, action, input) {
65
69
  const signal = AbortSignal.timeout(15e3);
@@ -76,11 +80,14 @@ function ontologyTools(options = {}) {
76
80
  return object(result.structuredContent);
77
81
  };
78
82
  const [caller, destinations] = await Promise.all([read("me", {}), read("chat_list", { chat: input.chat })]);
79
- return describeChatPost({
80
- values: input,
81
- caller,
82
- destinations
83
- });
83
+ return {
84
+ ...describeChatPost({
85
+ values: input,
86
+ caller,
87
+ destinations
88
+ }),
89
+ action: typeof tool.title === "string" ? tool.title : "❓"
90
+ };
84
91
  }
85
92
  let entities = [];
86
93
  try {