@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.
- package/README.md +10 -8
- package/dist/_chunks/{inbound-request-DRNPIIpb.mjs → inbound-request-CysnjMdw.mjs} +62 -62
- package/dist/extension/config.d.ts +7 -8
- package/dist/extension/config.mjs +20 -2
- package/dist/extension/instructions.md +8 -1
- package/dist/extension/lib/chat-post-confirmation.mjs +1 -1
- package/dist/extension/lib/confirmation-description.d.ts +1 -0
- package/dist/extension/lib/confirmation-description.mjs +29 -13
- package/dist/extension/lib/confirmations.d.ts +3 -0
- package/dist/extension/lib/confirmations.mjs +22 -5
- package/dist/extension/lib/customer-text-safety.d.ts +1 -0
- package/dist/extension/lib/customer-text-safety.mjs +23 -0
- package/dist/extension/lib/ontology-tools.mjs +13 -6
- package/dist/extension/lib/telegram/adapter.mjs +10 -6
- package/dist/extension/lib/telegram/attachments-send.mjs +6 -6
- package/dist/extension/lib/telegram/chat-post-card.d.ts +6 -13
- package/dist/extension/lib/telegram/chat-post-card.mjs +29 -37
- package/dist/extension/lib/telegram/confirmation-text.d.ts +20 -8
- package/dist/extension/lib/telegram/confirmation-text.mjs +55 -65
- package/dist/extension/lib/telegram/confirmations.d.ts +1 -2
- package/dist/extension/lib/telegram/confirmations.mjs +40 -96
- package/dist/extension/lib/telegram/content.d.ts +1 -1
- package/dist/extension/lib/telegram/conversations.mjs +1 -1
- package/dist/extension/lib/telegram/customer-text.d.ts +5 -5
- package/dist/extension/lib/telegram/customer-text.mjs +2 -1
- package/dist/extension/lib/telegram/files.mjs +1 -1
- package/dist/extension/lib/telegram/images.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
- package/dist/extension/lib/telegram/inbound.mjs +5 -5
- package/dist/extension/lib/telegram/media-policy.mjs +1 -1
- package/dist/extension/lib/telegram/principal.mjs +1 -1
- package/dist/extension/lib/telegram/silent.mjs +2 -2
- package/dist/extension/lib/telegram/turn-errors.mjs +2 -2
- package/dist/extension/lib/telegram/voice.mjs +1 -1
- 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
|
|
83
|
-
|
|
|
84
|
-
| `channels.telegram.connection`
|
|
85
|
-
| `channels.telegram.policy`
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `models.
|
|
89
|
-
| `
|
|
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 {
|
|
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(
|
|
27
|
-
return
|
|
28
|
-
failed: "
|
|
29
|
-
recovery: "
|
|
30
|
-
attachment: "
|
|
31
|
-
authorization: "
|
|
32
|
-
response: "
|
|
33
|
-
|
|
34
|
-
|
|
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",
|
|
57
|
+
function failureText(errorId, kind = "failed", _locale = currentLocale()) {
|
|
81
58
|
if (kind === "cancelled") return `⏹ ${errorId}`;
|
|
82
|
-
|
|
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)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
} :
|
|
136
|
+
entities: existing ?? []
|
|
137
|
+
} : guardedCustomerMessage(body[field], currentLocale(), () => {
|
|
138
|
+
textBlocked = true;
|
|
139
|
+
});
|
|
152
140
|
const guarded = customerText(message.text);
|
|
153
|
-
|
|
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))
|
|
164
|
-
if (typeof
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
|
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: [
|
|
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.
|
|
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: "
|
|
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.
|
|
16
|
-
if (text === "" ||
|
|
17
|
-
|
|
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)
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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 :
|
|
70
|
+
title = value === item ? void 0 : factText(value, 3584);
|
|
57
71
|
}
|
|
58
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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(
|
|
60
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 {
|