@inline-chat/hermes-agent-adapter 0.0.20 → 0.0.22-alpha.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 +70 -26
- package/package.json +1 -1
- package/plugin/inline/adapter.py +362 -101
- package/plugin/inline/cli.py +7 -5
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +59 -45
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Supported:
|
|
|
25
25
|
- `INLINE_TOKEN`, `INLINE_BOT_TOKEN`, `platforms.inline.token`, and `inline.token` auth paths, including simple `${ENV_NAME}` config references.
|
|
26
26
|
- Supervised loopback Node sidecar using the Inline realtime SDK.
|
|
27
27
|
- Realtime inbound messages, catch-up, replies to bot messages, and action callbacks.
|
|
28
|
+
- Inbound SDK receipts remain pending until Python handles the event. Temporary routing-metadata and authorization failures retry before deduplication or effects, preserving per-chat order while other chats can progress. Lost acknowledgement responses retry only the acknowledgement. Agent-button preflight retries are bounded, then show a retry prompt so an unavailable button cannot indefinitely block the shared action cursor. Shutdown leaves unresolved receipts available for catch-up; this is an at-least-once handoff, not a durable exactly-once guarantee for model turns or external effects.
|
|
28
29
|
- Outbound text, Markdown parsing, opt-in edit-message streaming, long-message splitting, edits, deletes, typing, and presence.
|
|
29
30
|
- Inline reply-thread routing, explicit-request auto mode, `/threads` controls, explicit `/follow` and `/unfollow` dialog relevance controls, parent chat metadata, parent/thread prompt fallback, and thread-specific skill bindings.
|
|
30
31
|
- Native Hermes `inline` tool for current-chat/thread reads, bounded history and search, exact message lookup, button-message sends, editing/deleting bot-owned messages, reactions, pin/unpin/list pins, reply-thread creation, top-level thread/chat creation outside the current conversation, and avatar presence/status.
|
|
@@ -33,7 +34,7 @@ Supported:
|
|
|
33
34
|
- DM and group policies, user allowlists, group sender allowlists, mention requirements, strict mention mode, allowed chats, and free-response chats.
|
|
34
35
|
- Native Inline `/` command-menu sync for Hermes slash commands, including `/threads`, `/follow`, `/unfollow`, `/inline_sync`, `/inline_version`, and `/update`; typed slash commands continue to work even if menu sync is disabled or rejected.
|
|
35
36
|
- Inline-native buttons for clarify prompts, command approvals, slash confirmations, and model selection.
|
|
36
|
-
- Agent-created `send_message`/`edit_message` button rows with opaque callback data. A callback is acknowledged
|
|
37
|
+
- Agent-created `send_message`/`edit_message` button rows with opaque callback data. A callback is acknowledged after its target and access checks succeed, then enters Hermes as a normal turn naming the source message and exact action fields. The normal response edits that source message and clears omitted buttons; the agent can instead call `edit_message` with replacement buttons and finish with `NO_REPLY` so the explicit edit remains authoritative.
|
|
37
38
|
- Outbound local photo, video, voice, and document uploads with configurable size caps.
|
|
38
39
|
- Inbound photo, video, voice, and document summaries, with URL-backed media cached locally for Hermes when available.
|
|
39
40
|
- Reactions on bot messages, plus opt-in lifecycle/system events as synthetic Hermes messages.
|
|
@@ -50,6 +51,18 @@ Unsupported or intentionally limited:
|
|
|
50
51
|
- Live voice sessions or calls. Voice file messages are supported, but realtime audio is not.
|
|
51
52
|
- Media without a usable Inline CDN/local URL. Those messages still produce text summaries, but Hermes may not receive a local file path.
|
|
52
53
|
|
|
54
|
+
## Alpha 0.0.22-alpha.0
|
|
55
|
+
|
|
56
|
+
This prerelease includes typed service-notice filtering before agent admission,
|
|
57
|
+
current shared-session setup guidance, and the pinned realtime SDK `0.0.18`.
|
|
58
|
+
Hermes keeps its supported agent activity surface; unsupported experimental tool
|
|
59
|
+
timelines remain disabled.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npm install -g @inline-chat/hermes-agent-adapter@0.0.22-alpha.0
|
|
63
|
+
inline-hermes install --force
|
|
64
|
+
```
|
|
65
|
+
|
|
53
66
|
## Install
|
|
54
67
|
|
|
55
68
|
```sh
|
|
@@ -155,7 +168,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
155
168
|
Expected local output includes:
|
|
156
169
|
|
|
157
170
|
```text
|
|
158
|
-
enabled user 0.0.
|
|
171
|
+
enabled user 0.0.21 inline-platform
|
|
159
172
|
```
|
|
160
173
|
|
|
161
174
|
## Update Or Reinstall
|
|
@@ -253,26 +266,14 @@ platforms:
|
|
|
253
266
|
enabled: true
|
|
254
267
|
```
|
|
255
268
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
`display.tool_progress: all`, add the per-platform override below so terminal
|
|
260
|
-
and tool progress bubbles are not left in Inline chats:
|
|
261
|
-
|
|
262
|
-
```yaml
|
|
263
|
-
display:
|
|
264
|
-
platforms:
|
|
265
|
-
inline:
|
|
266
|
-
tool_progress: off
|
|
267
|
-
cleanup_progress: true
|
|
268
|
-
streaming: false
|
|
269
|
-
interim_assistant_messages: false
|
|
270
|
-
```
|
|
269
|
+
Experimental tool timelines are unavailable in this adapter. The old
|
|
270
|
+
`INLINE_EXPERIMENTAL_AGENT_ACTIVITY` setting is ignored and logs a warning when
|
|
271
|
+
enabled. Hermes uses ordinary typing, optional processing reactions, and replies.
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
progress
|
|
275
|
-
|
|
273
|
+
Inline keeps tool progress and interim assistant messages quiet by default.
|
|
274
|
+
Explicit Hermes display settings still take precedence; if you enable native
|
|
275
|
+
tool progress, Hermes uses its ordinary progress messages. Token streaming remains
|
|
276
|
+
opt-in through both `streaming.enabled: true` and
|
|
276
277
|
`display.platforms.inline.streaming: true`.
|
|
277
278
|
|
|
278
279
|
Hermes-native sends and scheduled deliveries accept Inline's explicit target
|
|
@@ -334,6 +335,22 @@ Access control follows Hermes' native platform model:
|
|
|
334
335
|
|
|
335
336
|
Policy is evaluated in three ordered stages: **access**, then **wake**, then **delivery**. Access checks the chat and sender policy and is a hard gate; a mention, reply, callback, or command never grants access to a blocked chat or actor. Wake decides whether an allowed group turn invokes Hermes: free-response chats wake normally, while mention-gated chats require an explicit mention unless a configured reply-to-bot or followed-thread exception applies. Delivery keeps existing child-thread conversations in place; top-level `auto` creates a child only for explicit thread intent, `on` always creates one, and `off` stays flat.
|
|
336
337
|
|
|
338
|
+
`open` controls intake; it does not grant permission to use the bot. After
|
|
339
|
+
Inline's sender and group/thread restrictions pass, the adapter asks Hermes'
|
|
340
|
+
registered authorization callback before executing local commands, creating
|
|
341
|
+
reply threads, downloading media, collecting context, or exposing runtime
|
|
342
|
+
settings. This uses the same pairing and profile-aware authorization as native
|
|
343
|
+
Hermes adapters. An unapproved sender reaches Hermes' pairing or rejection
|
|
344
|
+
flow with a minimal text event and no preceding adapter mutations or media work.
|
|
345
|
+
Pairing approval does not override an explicit Inline allowlist, disabled
|
|
346
|
+
policy, or excluded group. DMs remain exempt from `allowed_chats`.
|
|
347
|
+
Child-thread authorization preserves both the child and parent chat IDs so
|
|
348
|
+
Hermes can select the correct profile. If required group metadata is unavailable,
|
|
349
|
+
the adapter denies the operation instead of guessing a profile. Controls that
|
|
350
|
+
change the parent's reply-thread mode also require access to that parent;
|
|
351
|
+
thread-local model and following settings remain available to an authorized
|
|
352
|
+
child-thread user.
|
|
353
|
+
|
|
337
354
|
Equivalent Hermes YAML can use `allow_from`, `allowed_users`,
|
|
338
355
|
`group_allow_from`, `dm_policy`, `group_policy`, `require_mention`,
|
|
339
356
|
`strict_mention`, `allowed_chats`, `free_response_chats`, `reply_threads`,
|
|
@@ -361,11 +378,14 @@ platforms:
|
|
|
361
378
|
skills: ["support-triage", "incident-report"]
|
|
362
379
|
```
|
|
363
380
|
|
|
364
|
-
Inline-native button callbacks,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
Inline-native button callbacks, including approvals, clarify choices,
|
|
382
|
+
model pickers, and thread controls, use the same local restrictions and Hermes
|
|
383
|
+
authorization callback. Pairing-approved users can use them without a second
|
|
384
|
+
Inline allowlist. A denial, error, or unknown result from a registered callback
|
|
385
|
+
blocks the action. Only standalone adapters without a registered callback use
|
|
386
|
+
explicit Inline/global allowlists or allow-all settings as a fallback; `open`
|
|
387
|
+
alone never authorizes controls. Settings requests from unauthorized users show
|
|
388
|
+
an access guide without runtime or model information.
|
|
369
389
|
Adapter-owned controls use `system:` action IDs and stay in these deterministic
|
|
370
390
|
handlers. Agent-authored callbacks use `agent:` IDs and follow the ordinary
|
|
371
391
|
message intake policy because they are conversational input, not approval or
|
|
@@ -416,6 +436,30 @@ the normal reply.
|
|
|
416
436
|
The plugin id is `inline`, which is intentionally the same id an eventual
|
|
417
437
|
bundled Hermes adapter should use.
|
|
418
438
|
|
|
439
|
+
## Shared transcripts in groups and threads
|
|
440
|
+
|
|
441
|
+
To share one conversation transcript among admitted participants in a group
|
|
442
|
+
or thread, add these keys at the root of the `config.yaml` used by the Hermes
|
|
443
|
+
gateway process, outside `platforms.inline.extra`:
|
|
444
|
+
|
|
445
|
+
```yaml
|
|
446
|
+
group_sessions_per_user: false
|
|
447
|
+
thread_sessions_per_user: false
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Hermes defaults `group_sessions_per_user` to `true` (groups split by sender)
|
|
451
|
+
and `thread_sessions_per_user` to `false` (threads shared). These settings
|
|
452
|
+
affect every messaging platform governed by that gateway configuration.
|
|
453
|
+
For separate profile gateways, configure each gateway's profile separately.
|
|
454
|
+
With a multiplexed gateway, do not assume a secondary profile's values control
|
|
455
|
+
its sessions; this adapter does not qualify per-profile session-policy overrides.
|
|
456
|
+
|
|
457
|
+
Distinct Hermes profile/gateway namespaces keep separate conversation sessions.
|
|
458
|
+
Swapping a bot token within one profile does not create a new transcript namespace.
|
|
459
|
+
These flags select shared sessions; they do not merge existing histories, import
|
|
460
|
+
all Inline history, grant permissions, or change access, mention, or bot wake rules.
|
|
461
|
+
Restart the gateway through its existing process manager after editing.
|
|
462
|
+
|
|
419
463
|
## Error Reporting And Privacy
|
|
420
464
|
|
|
421
465
|
The Inline adapter and its supervised sidecar keep error reporting disabled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inline-chat/hermes-agent-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22-alpha.0",
|
|
4
4
|
"description": "Hermes Agent platform adapter for Inline, with a native Python plugin and bundled Inline realtime sidecar.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -70,7 +70,10 @@ _DEDUP_MAX_SIZE = 5000
|
|
|
70
70
|
_DEDUP_WINDOW_SECONDS = 48 * 3600
|
|
71
71
|
_CHAT_INFO_CACHE_SECONDS = 10 * 60
|
|
72
72
|
_CHAT_INFO_CACHE_MAX_SIZE = 512
|
|
73
|
-
|
|
73
|
+
_CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS = 2.0
|
|
74
|
+
_INBOUND_ACCESS_LOOKUP_TIMEOUT_SECONDS = 30.0
|
|
75
|
+
_INBOUND_RETRY_INITIAL_SECONDS = 1.0
|
|
76
|
+
_MAX_INBOUND_DELIVERIES = 16
|
|
74
77
|
# Provider discovery may invoke host CLIs. Keep repeated panel opens and
|
|
75
78
|
# unrelated mutations off that slow path while always injecting the live
|
|
76
79
|
# current/default model into the document below.
|
|
@@ -191,6 +194,10 @@ _INLINE_DISPLAY_DEFAULTS = {
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
|
|
197
|
+
class InlineInboundDeferred(RuntimeError):
|
|
198
|
+
"""Pre-effect dependency unavailable; keep the SDK delivery unacknowledged."""
|
|
199
|
+
|
|
200
|
+
|
|
194
201
|
class InlineSidecarError(RuntimeError):
|
|
195
202
|
def __init__(self, path: str, status_code: int, message: str, error_kind: str = "unknown", raw: Optional[Any] = None):
|
|
196
203
|
self.path = path
|
|
@@ -223,11 +230,12 @@ def _install_inline_display_defaults() -> None:
|
|
|
223
230
|
defaults = getattr(_display_config, "_PLATFORM_DEFAULTS", None)
|
|
224
231
|
if not isinstance(defaults, dict):
|
|
225
232
|
return
|
|
233
|
+
inline_defaults = dict(_INLINE_DISPLAY_DEFAULTS)
|
|
226
234
|
current = defaults.get("inline")
|
|
227
235
|
if isinstance(current, dict):
|
|
228
|
-
defaults["inline"] = {**
|
|
236
|
+
defaults["inline"] = {**inline_defaults, **current}
|
|
229
237
|
else:
|
|
230
|
-
defaults["inline"] =
|
|
238
|
+
defaults["inline"] = inline_defaults
|
|
231
239
|
except Exception:
|
|
232
240
|
logger.debug("[inline] failed to install display defaults", exc_info=True)
|
|
233
241
|
|
|
@@ -805,6 +813,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
805
813
|
super().__init__(config, Platform("inline"))
|
|
806
814
|
extra = config.extra or {}
|
|
807
815
|
|
|
816
|
+
if _truthy(os.getenv("INLINE_EXPERIMENTAL_AGENT_ACTIVITY")):
|
|
817
|
+
logger.warning("[inline] experimental tool timelines are unavailable; ignoring INLINE_EXPERIMENTAL_AGENT_ACTIVITY and using ordinary typing")
|
|
808
818
|
self._token = _config_token(config)
|
|
809
819
|
self._base_url = os.getenv("INLINE_BASE_URL") or extra.get("base_url") or "https://api.inline.chat"
|
|
810
820
|
self._sidecar_bind = _normalize_sidecar_bind(extra.get("sidecar_bind") or os.getenv("INLINE_SIDECAR_BIND"))
|
|
@@ -973,6 +983,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
973
983
|
self._last_catalog_sync: Optional[Dict[str, Any]] = None
|
|
974
984
|
self._inbound_task: Optional[asyncio.Task] = None
|
|
975
985
|
self._bot_settings_tasks: set[asyncio.Task] = set()
|
|
986
|
+
self._inbound_deliveries: Dict[str, asyncio.Task] = {}
|
|
976
987
|
self._bot_settings_locks: Dict[str, asyncio.Lock] = {}
|
|
977
988
|
self._bot_settings_lock_users: Dict[str, int] = {}
|
|
978
989
|
self._bot_settings_model_catalog_cache: Optional[tuple[float, List[Dict[str, Any]]]] = None
|
|
@@ -1147,7 +1158,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1147
1158
|
try:
|
|
1148
1159
|
info = await asyncio.wait_for(
|
|
1149
1160
|
self._get_chat_info(chat_id),
|
|
1150
|
-
timeout=
|
|
1161
|
+
timeout=_CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS,
|
|
1151
1162
|
)
|
|
1152
1163
|
except asyncio.TimeoutError:
|
|
1153
1164
|
logger.warning("[inline] agent settings chat metadata timed out")
|
|
@@ -1163,13 +1174,15 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1163
1174
|
scope_chat_id = parent_chat_id or chat_id
|
|
1164
1175
|
is_reply_thread = bool(parent_chat_id)
|
|
1165
1176
|
chat_type = self._bot_settings_chat_type(info)
|
|
1166
|
-
can_inspect = self.
|
|
1167
|
-
chat_id,
|
|
1168
|
-
chat_id if is_reply_thread else None,
|
|
1169
|
-
parent_chat_id,
|
|
1177
|
+
can_inspect = self._actor_authorization(
|
|
1178
|
+
chat_type, actor_id, chat_id,
|
|
1179
|
+
thread_id=chat_id if is_reply_thread else None,
|
|
1180
|
+
parent_chat_id=parent_chat_id,
|
|
1181
|
+
is_bot=_inline_sender_profile(event).get("bot") is True,
|
|
1170
1182
|
)
|
|
1171
|
-
if not
|
|
1172
|
-
return {"access": "guideOnly", "scope_id": scope_chat_id, "reply_threads": "auto"
|
|
1183
|
+
if can_inspect is not True:
|
|
1184
|
+
return {"access": "guideOnly", "scope_id": scope_chat_id, "reply_threads": "auto",
|
|
1185
|
+
**({"unavailable_reason": "authorization"} if can_inspect is None else {})}
|
|
1173
1186
|
|
|
1174
1187
|
source = self.build_source(
|
|
1175
1188
|
chat_id=chat_id,
|
|
@@ -1180,7 +1193,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1180
1193
|
parent_chat_id=parent_chat_id,
|
|
1181
1194
|
)
|
|
1182
1195
|
runner = self._bot_settings_runner()
|
|
1183
|
-
can_modify =
|
|
1196
|
+
can_modify = can_inspect
|
|
1197
|
+
can_set_reply_threads = not is_reply_thread or await self._chat_actor_authorized(scope_chat_id, actor_id)
|
|
1184
1198
|
context: Dict[str, Any] = {
|
|
1185
1199
|
"access": "full" if can_modify else "readOnly",
|
|
1186
1200
|
"scope_id": scope_chat_id,
|
|
@@ -1189,7 +1203,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1189
1203
|
"chat_type": chat_type,
|
|
1190
1204
|
"is_reply_thread": is_reply_thread,
|
|
1191
1205
|
"following": None if chat_type == "dm" else self._chat_follow_mode_following(info),
|
|
1192
|
-
"reply_threads": self._reply_thread_mode_for_chat(scope_chat_id),
|
|
1206
|
+
"reply_threads": self._reply_thread_mode_for_chat(scope_chat_id) if can_set_reply_threads else None,
|
|
1207
|
+
"can_set_reply_threads": can_set_reply_threads,
|
|
1193
1208
|
"can_set_default_model": can_modify,
|
|
1194
1209
|
"source": source,
|
|
1195
1210
|
"runner": runner,
|
|
@@ -1323,7 +1338,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1323
1338
|
if context.get("access") == "guideOnly":
|
|
1324
1339
|
unavailable_text = (
|
|
1325
1340
|
"Hermes could not verify this chat. Try again when the bot is reachable."
|
|
1326
|
-
if context.get("unavailable_reason")
|
|
1341
|
+
if context.get("unavailable_reason") in {"chat_metadata", "authorization"}
|
|
1327
1342
|
else "This chat is not allowed by Hermes' access policy."
|
|
1328
1343
|
)
|
|
1329
1344
|
return {
|
|
@@ -1386,15 +1401,26 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1386
1401
|
"id": "following", "label": "Following", "description": "Wake on eligible activity.", **common,
|
|
1387
1402
|
"control": {"oneofKind": "toggle", "toggle": {"value": bool(context.get("following"))}},
|
|
1388
1403
|
}]})
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
"
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1404
|
+
if context.get("can_set_reply_threads"):
|
|
1405
|
+
sections.append({"id": "replies", "items": [{
|
|
1406
|
+
"id": "reply-threads", "label": "Reply in threads", **common,
|
|
1407
|
+
"control": {"oneofKind": "select", "select": {
|
|
1408
|
+
"value": context.get("reply_threads") or "auto",
|
|
1409
|
+
"options": [
|
|
1410
|
+
{"value": "auto", "label": "Auto", "description": "Agent decides.", "disabled": False},
|
|
1411
|
+
{"value": "on", "label": "On", "description": "Always use threads.", "disabled": False},
|
|
1412
|
+
{"value": "off", "label": "Off", "description": "Stay in chat.", "disabled": False},
|
|
1413
|
+
],
|
|
1414
|
+
}},
|
|
1415
|
+
}]})
|
|
1416
|
+
else:
|
|
1417
|
+
sections.append({"id": "replies", "items": [{
|
|
1418
|
+
"id": "reply-threads-access", "label": "Reply in threads", "disabled": False,
|
|
1419
|
+
"control": {"oneofKind": "info", "info": {
|
|
1420
|
+
"text": ("Parent chat access is temporarily unavailable. Try again."
|
|
1421
|
+
if context.get("can_set_reply_threads") is None
|
|
1422
|
+
else "Changing reply mode requires access to the parent chat."),
|
|
1423
|
+
"tone": _INLINE_BOT_SETTINGS_TONE_WARNING,
|
|
1398
1424
|
}},
|
|
1399
1425
|
}]})
|
|
1400
1426
|
if disabled:
|
|
@@ -1492,6 +1518,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1492
1518
|
self._invalidate_chat_info(context["chat_id"])
|
|
1493
1519
|
return
|
|
1494
1520
|
if item_id == "reply-threads" and value_body.get("oneofKind") == "stringValue":
|
|
1521
|
+
if not context.get("can_set_reply_threads"):
|
|
1522
|
+
raise ValueError("parent chat thread settings are not authorized")
|
|
1495
1523
|
mode = str(value_body.get("stringValue") or "")
|
|
1496
1524
|
if mode not in {"auto", "on", "off"}:
|
|
1497
1525
|
raise ValueError("invalid reply mode")
|
|
@@ -1768,6 +1796,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1768
1796
|
*,
|
|
1769
1797
|
chat_id: str,
|
|
1770
1798
|
msg_id: str,
|
|
1799
|
+
from_id: str,
|
|
1771
1800
|
text: str,
|
|
1772
1801
|
chat_type: str,
|
|
1773
1802
|
thread_id: Optional[str],
|
|
@@ -1779,6 +1808,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1779
1808
|
|
|
1780
1809
|
metadata = {"thread_id": thread_id} if thread_id else None
|
|
1781
1810
|
target_chat_id = parent_chat_id or chat_id
|
|
1811
|
+
if target_chat_id != chat_id and not await self._chat_actor_authorized(target_chat_id, from_id):
|
|
1812
|
+
await self.send(chat_id, "Parent chat access could not be confirmed. Check access and try again.", reply_to=msg_id, metadata=metadata)
|
|
1813
|
+
return True
|
|
1782
1814
|
if action == "on":
|
|
1783
1815
|
self._set_reply_threads_for_chat(target_chat_id, "on")
|
|
1784
1816
|
elif action == "off":
|
|
@@ -1935,6 +1967,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1935
1967
|
except Exception:
|
|
1936
1968
|
pass
|
|
1937
1969
|
self._inbound_task = None
|
|
1970
|
+
deliveries = list(self._inbound_deliveries.values())
|
|
1971
|
+
for task in deliveries:
|
|
1972
|
+
task.cancel()
|
|
1973
|
+
if deliveries:
|
|
1974
|
+
await asyncio.gather(*deliveries, return_exceptions=True)
|
|
1975
|
+
self._inbound_deliveries.clear()
|
|
1938
1976
|
for task in list(self._bot_settings_tasks):
|
|
1939
1977
|
task.cancel()
|
|
1940
1978
|
if self._bot_settings_tasks:
|
|
@@ -2247,6 +2285,78 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2247
2285
|
event = json.loads(line)
|
|
2248
2286
|
except json.JSONDecodeError:
|
|
2249
2287
|
return
|
|
2288
|
+
delivery_id = event.get("_inlineDeliveryId")
|
|
2289
|
+
if not isinstance(delivery_id, str) or not delivery_id:
|
|
2290
|
+
await self._dispatch_inbound(event)
|
|
2291
|
+
return
|
|
2292
|
+
# The SDK bounds outstanding receipts and orders each chat. Retain only
|
|
2293
|
+
# their active coroutines; reconnect replay must not run one twice.
|
|
2294
|
+
if delivery_id in self._inbound_deliveries:
|
|
2295
|
+
return
|
|
2296
|
+
# An ACK may reach the sidecar while its response is lost, releasing an
|
|
2297
|
+
# SDK slot before this coroutine finishes. Bound those extra ACK waiters
|
|
2298
|
+
# too, using stream backpressure rather than another event queue.
|
|
2299
|
+
while len(self._inbound_deliveries) >= _MAX_INBOUND_DELIVERIES:
|
|
2300
|
+
await asyncio.wait(tuple(self._inbound_deliveries.values()), return_when=asyncio.FIRST_COMPLETED)
|
|
2301
|
+
task = asyncio.create_task(self._consume_inbound_delivery(delivery_id, event))
|
|
2302
|
+
self._inbound_deliveries[delivery_id] = task
|
|
2303
|
+
|
|
2304
|
+
def finished(completed: asyncio.Task) -> None:
|
|
2305
|
+
self._inbound_deliveries.pop(delivery_id, None)
|
|
2306
|
+
if not completed.cancelled():
|
|
2307
|
+
try:
|
|
2308
|
+
completed.result()
|
|
2309
|
+
except Exception as exc:
|
|
2310
|
+
self._report_error("inbound.delivery", exc)
|
|
2311
|
+
task.add_done_callback(finished)
|
|
2312
|
+
|
|
2313
|
+
async def _consume_inbound_delivery(self, delivery_id: str, event: Dict[str, Any]) -> None:
|
|
2314
|
+
try:
|
|
2315
|
+
await self._handle_inbound_delivery(delivery_id, event)
|
|
2316
|
+
except Exception as exc:
|
|
2317
|
+
# Do not ACK an unexpected failure or silently pin its chat forever.
|
|
2318
|
+
# Native notification shields runner teardown/reconnect from the
|
|
2319
|
+
# cancellation of this task during adapter.disconnect().
|
|
2320
|
+
self._report_error("inbound.delivery", exc)
|
|
2321
|
+
self._set_fatal_error("INBOUND_FAILED", str(exc), retryable=True)
|
|
2322
|
+
await self._notify_fatal_error()
|
|
2323
|
+
|
|
2324
|
+
async def _handle_inbound_delivery(self, delivery_id: str, event: Dict[str, Any]) -> None:
|
|
2325
|
+
delay = _INBOUND_RETRY_INITIAL_SECONDS
|
|
2326
|
+
attempts = 0
|
|
2327
|
+
while True:
|
|
2328
|
+
attempts += 1
|
|
2329
|
+
try:
|
|
2330
|
+
await self._dispatch_inbound(event)
|
|
2331
|
+
break
|
|
2332
|
+
except InlineSidecarError as exc:
|
|
2333
|
+
if exc.error_kind not in {"forbidden", "not_found"}:
|
|
2334
|
+
raise
|
|
2335
|
+
logger.info("[inline] inbound target is no longer accessible; retiring delivery")
|
|
2336
|
+
break
|
|
2337
|
+
except InlineInboundDeferred as exc:
|
|
2338
|
+
# Action events share the SDK's user-cursor barrier. A persistent
|
|
2339
|
+
# outage must not hold every chat behind a stale button press.
|
|
2340
|
+
if event.get("kind") == "message.action.invoke" and attempts >= 2:
|
|
2341
|
+
await self._answer_action(str(event.get("interactionId") or ""),
|
|
2342
|
+
"Temporarily unavailable. Please try again.")
|
|
2343
|
+
break
|
|
2344
|
+
logger.warning("[inline] inbound preflight unavailable; retrying in %.1fs: %s", delay, exc)
|
|
2345
|
+
await asyncio.sleep(delay)
|
|
2346
|
+
delay = min(delay * 2, 30.0)
|
|
2347
|
+
# A lost ACK response must retry only the ACK, never local commands or
|
|
2348
|
+
# model delivery. The endpoint is idempotent, including after reconnect.
|
|
2349
|
+
delay = _INBOUND_RETRY_INITIAL_SECONDS
|
|
2350
|
+
while True:
|
|
2351
|
+
try:
|
|
2352
|
+
await self._sidecar_call("/inbound/ack", {"deliveryId": delivery_id})
|
|
2353
|
+
return
|
|
2354
|
+
except Exception:
|
|
2355
|
+
logger.warning("[inline] inbound acknowledgement unavailable; retrying in %.1fs", delay)
|
|
2356
|
+
await asyncio.sleep(delay)
|
|
2357
|
+
delay = min(delay * 2, 30.0)
|
|
2358
|
+
|
|
2359
|
+
async def _dispatch_inbound(self, event: Dict[str, Any]) -> None:
|
|
2250
2360
|
self._me_id = str(event.get("meId") or self._me_id or "") or None
|
|
2251
2361
|
self._me_username = _normalize_inline_username(event.get("meUsername") or self._me_username)
|
|
2252
2362
|
# Chat snapshots include mutable dialog and routing fields such as
|
|
@@ -2257,10 +2367,16 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2257
2367
|
self._invalidate_chat_info(event.get("chatId"))
|
|
2258
2368
|
kind = event.get("kind")
|
|
2259
2369
|
if kind == "bot.chatSettings.request":
|
|
2260
|
-
|
|
2370
|
+
if event.get("_inlineDeliveryId"):
|
|
2371
|
+
await self._handle_bot_settings_request(event)
|
|
2372
|
+
else:
|
|
2373
|
+
self._schedule_bot_settings_task(self._handle_bot_settings_request(event))
|
|
2261
2374
|
return
|
|
2262
2375
|
if kind == "bot.chatSettings.item.invoke":
|
|
2263
|
-
|
|
2376
|
+
if event.get("_inlineDeliveryId"):
|
|
2377
|
+
await self._handle_bot_settings_item(event)
|
|
2378
|
+
else:
|
|
2379
|
+
self._schedule_bot_settings_task(self._handle_bot_settings_item(event))
|
|
2264
2380
|
return
|
|
2265
2381
|
if kind == "message.action.invoke":
|
|
2266
2382
|
if await self._handle_action(event):
|
|
@@ -2331,7 +2447,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2331
2447
|
|
|
2332
2448
|
async def _dispatch_message(self, event: Dict[str, Any], *, edit: bool = False) -> None:
|
|
2333
2449
|
agent_action = event.get("_inlineAgentAction") if isinstance(event.get("_inlineAgentAction"), dict) else None
|
|
2334
|
-
msg = event.get("message")
|
|
2450
|
+
msg = event.get("message")
|
|
2451
|
+
if not isinstance(msg, dict):
|
|
2452
|
+
return
|
|
2453
|
+
# History/join recovery and older sidecars can reach this entry directly.
|
|
2454
|
+
# Service fallback text and mentions are never conversational direction.
|
|
2455
|
+
raw = msg.get("raw") if isinstance(msg.get("raw"), dict) else {}
|
|
2456
|
+
if isinstance(msg.get("serviceMessage"), dict) or isinstance(raw.get("serviceMessage"), dict):
|
|
2457
|
+
return
|
|
2335
2458
|
msg_id = str(msg.get("id") or "")
|
|
2336
2459
|
chat_id = str(event.get("chatId") or msg.get("chatId") or "")
|
|
2337
2460
|
if not msg_id or not chat_id:
|
|
@@ -2349,10 +2472,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2349
2472
|
dedup_key = f"new:{chat_id}:msg:{msg_id}:date:{message_date}"
|
|
2350
2473
|
else:
|
|
2351
2474
|
dedup_key = f"new:{chat_id}:msg:{msg_id}"
|
|
2352
|
-
if self._is_duplicate(dedup_key):
|
|
2353
|
-
return
|
|
2354
|
-
if not edit and self._is_duplicate_message_instance(chat_id, msg, event):
|
|
2355
|
-
return
|
|
2356
2475
|
from_id = str(msg.get("fromId") or "")
|
|
2357
2476
|
if msg.get("out") or (self._me_id and from_id == self._me_id):
|
|
2358
2477
|
return
|
|
@@ -2361,18 +2480,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2361
2480
|
|
|
2362
2481
|
raw_message_text = str(msg.get("message") or "")
|
|
2363
2482
|
text = raw_message_text.strip()
|
|
2364
|
-
media_text, media_urls, media_types, message_type = await self._normalize_media(msg)
|
|
2365
|
-
if media_text:
|
|
2366
|
-
text = f"{text}\n{media_text}".strip() if text else media_text
|
|
2367
|
-
if not text and not media_urls:
|
|
2368
|
-
text = "[Inline message with no text]"
|
|
2369
2483
|
explicitly_mentions_me = self._message_entity_mentions_me(msg)
|
|
2370
|
-
if event.get("_inlineSenderProvenanceVerified") is False and not explicitly_mentions_me:
|
|
2371
|
-
self._remember_observed_context(chat_id, msg, text)
|
|
2372
|
-
return
|
|
2373
|
-
if sender_profile.get("bot") is True and not explicitly_mentions_me:
|
|
2374
|
-
self._remember_observed_context(chat_id, msg, text)
|
|
2375
|
-
return
|
|
2376
2484
|
|
|
2377
2485
|
chat_type = self._chat_type_from_message(msg)
|
|
2378
2486
|
thread_id = self._thread_id_from_message(msg)
|
|
@@ -2381,7 +2489,20 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2381
2489
|
chat_name = chat_id
|
|
2382
2490
|
chat_info: Dict[str, Any] = {}
|
|
2383
2491
|
if chat_type == "group":
|
|
2384
|
-
|
|
2492
|
+
try:
|
|
2493
|
+
chat_info = await asyncio.wait_for(
|
|
2494
|
+
self._get_chat_info(chat_id, required=True),
|
|
2495
|
+
timeout=_CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS if agent_action else _INBOUND_ACCESS_LOOKUP_TIMEOUT_SECONDS,
|
|
2496
|
+
)
|
|
2497
|
+
except asyncio.TimeoutError as exc:
|
|
2498
|
+
raise InlineInboundDeferred("chat metadata timed out") from exc
|
|
2499
|
+
except InlineSidecarError as exc:
|
|
2500
|
+
if exc.error_kind in {"forbidden", "not_found"}:
|
|
2501
|
+
logger.info("[inline] chat is no longer accessible; retiring inbound delivery")
|
|
2502
|
+
return
|
|
2503
|
+
raise
|
|
2504
|
+
if not chat_info:
|
|
2505
|
+
raise InlineInboundDeferred("chat metadata unavailable")
|
|
2385
2506
|
chat_name = self._chat_title_from_info(chat_info) or chat_id
|
|
2386
2507
|
info_parent_chat_id = self._chat_info_id(chat_info, "parentChatId")
|
|
2387
2508
|
info_parent_message_id = self._chat_info_id(chat_info, "parentMessageId")
|
|
@@ -2402,9 +2523,52 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2402
2523
|
)
|
|
2403
2524
|
if has_command_target and not command_addressed_to_me:
|
|
2404
2525
|
return
|
|
2526
|
+
actor_authorized = self._actor_authorization(
|
|
2527
|
+
chat_type, from_id, chat_id, thread_id=thread_id,
|
|
2528
|
+
parent_chat_id=parent_chat_id, is_bot=sender_profile.get("bot") is True,
|
|
2529
|
+
)
|
|
2530
|
+
if actor_authorized is None:
|
|
2531
|
+
raise InlineInboundDeferred("host authorization unavailable")
|
|
2532
|
+
if self._is_duplicate(dedup_key):
|
|
2533
|
+
return
|
|
2534
|
+
if not edit and self._is_duplicate_message_instance(chat_id, msg, event):
|
|
2535
|
+
return
|
|
2536
|
+
context_only = not explicitly_mentions_me and (
|
|
2537
|
+
event.get("_inlineSenderProvenanceVerified") is False or sender_profile.get("bot") is True
|
|
2538
|
+
)
|
|
2539
|
+
if not actor_authorized:
|
|
2540
|
+
if context_only:
|
|
2541
|
+
return
|
|
2542
|
+
# Let Hermes pair/ignore/decline the sender, without first downloading
|
|
2543
|
+
# media, collecting context, changing settings, or creating a thread.
|
|
2544
|
+
source = self.build_source(
|
|
2545
|
+
chat_id=chat_id, chat_name=chat_name, chat_type=chat_type,
|
|
2546
|
+
user_id=from_id, user_name=sender_name or None,
|
|
2547
|
+
thread_id=thread_id, parent_chat_id=parent_chat_id, message_id=msg_id,
|
|
2548
|
+
is_bot=sender_profile.get("bot") is True,
|
|
2549
|
+
)
|
|
2550
|
+
await self.handle_message(MessageEvent(
|
|
2551
|
+
text=text or "[Inline message with no text]", message_type=MessageType.TEXT,
|
|
2552
|
+
source=source, raw_message=event,
|
|
2553
|
+
message_id=build_inline_agent_action_turn_id(
|
|
2554
|
+
agent_action.get("messageId"), agent_action.get("interactionId"),
|
|
2555
|
+
) if agent_action else msg_id,
|
|
2556
|
+
timestamp=self._timestamp(event.get("date") or msg.get("date")),
|
|
2557
|
+
allow_gateway_control=not bool(agent_action),
|
|
2558
|
+
))
|
|
2559
|
+
return
|
|
2560
|
+
media_text, media_urls, media_types, message_type = await self._normalize_media(msg)
|
|
2561
|
+
if media_text:
|
|
2562
|
+
text = f"{text}\n{media_text}".strip() if text else media_text
|
|
2563
|
+
if not text and not media_urls:
|
|
2564
|
+
text = "[Inline message with no text]"
|
|
2565
|
+
if context_only:
|
|
2566
|
+
self._remember_observed_context(chat_id, msg, text)
|
|
2567
|
+
return
|
|
2405
2568
|
if not agent_action and await self._handle_thread_command(
|
|
2406
2569
|
chat_id=chat_id,
|
|
2407
2570
|
msg_id=msg_id,
|
|
2571
|
+
from_id=from_id,
|
|
2408
2572
|
text=text,
|
|
2409
2573
|
chat_type=chat_type,
|
|
2410
2574
|
thread_id=thread_id,
|
|
@@ -2584,6 +2748,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2584
2748
|
chat_type=chat_type,
|
|
2585
2749
|
user_id=from_id,
|
|
2586
2750
|
user_name=sender_name or None,
|
|
2751
|
+
is_bot=sender_profile.get("bot") is True,
|
|
2587
2752
|
thread_id=thread_id,
|
|
2588
2753
|
parent_chat_id=parent_chat_id,
|
|
2589
2754
|
message_id=msg_id,
|
|
@@ -2618,7 +2783,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2618
2783
|
|
|
2619
2784
|
async def _dispatch_agent_action(self, event: Dict[str, Any]) -> None:
|
|
2620
2785
|
interaction_id = str(event.get("interactionId") or "")
|
|
2621
|
-
await self._answer_action(interaction_id, "")
|
|
2622
2786
|
|
|
2623
2787
|
chat_id = str(event.get("chatId") or "")
|
|
2624
2788
|
message_id = str(event.get("messageId") or "")
|
|
@@ -2626,13 +2790,19 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2626
2790
|
if not chat_id or not message_id or not interaction_id or not actor_user_id:
|
|
2627
2791
|
logger.warning("[inline] ignored incomplete agent action event")
|
|
2628
2792
|
return
|
|
2629
|
-
|
|
2793
|
+
try:
|
|
2794
|
+
target = await asyncio.wait_for(
|
|
2795
|
+
self._fetch_message(chat_id, message_id, required=True),
|
|
2796
|
+
timeout=_CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS,
|
|
2797
|
+
)
|
|
2798
|
+
except asyncio.TimeoutError as exc:
|
|
2799
|
+
raise InlineInboundDeferred("action target lookup timed out") from exc
|
|
2630
2800
|
if not target:
|
|
2631
|
-
|
|
2801
|
+
await self._answer_action(interaction_id, "Action expired or no longer accessible.")
|
|
2632
2802
|
return
|
|
2633
2803
|
|
|
2634
2804
|
synthetic_message = dict(target)
|
|
2635
|
-
for stale_key in ("actions", "attachments", "entities", "media", "reactions", "replies"):
|
|
2805
|
+
for stale_key in ("actions", "attachments", "entities", "media", "reactions", "replies", "sender"):
|
|
2636
2806
|
synthetic_message.pop(stale_key, None)
|
|
2637
2807
|
synthetic_message.update({
|
|
2638
2808
|
"id": message_id,
|
|
@@ -2653,6 +2823,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2653
2823
|
"message": synthetic_message,
|
|
2654
2824
|
"_inlineAgentAction": dict(event),
|
|
2655
2825
|
})
|
|
2826
|
+
await self._answer_action(interaction_id, "")
|
|
2656
2827
|
|
|
2657
2828
|
def _message_explicitly_mentions_me(self, msg: Dict[str, Any]) -> bool:
|
|
2658
2829
|
if not self._me_id:
|
|
@@ -2758,13 +2929,19 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2758
2929
|
if self._me_id and user_id == self._me_id:
|
|
2759
2930
|
return
|
|
2760
2931
|
key = f"{event.get('kind')}:{chat_id}:{message_id}:{user_id}:{emoji}:{event.get('seq') or ''}"
|
|
2761
|
-
if self._is_duplicate(key):
|
|
2762
|
-
return
|
|
2763
2932
|
|
|
2764
2933
|
target = await self._fetch_message(chat_id, message_id)
|
|
2765
2934
|
chat_type = self._chat_type_from_message(target or {"peerId": {"type": {"oneofKind": "chat"}}})
|
|
2766
2935
|
if not self._allowed(chat_type, user_id):
|
|
2767
2936
|
return
|
|
2937
|
+
scope = await self._message_scope(chat_id, target or {}, required=True)
|
|
2938
|
+
if scope is None:
|
|
2939
|
+
return
|
|
2940
|
+
chat_type, thread_id, parent_chat_id = scope
|
|
2941
|
+
if chat_type == "group" and not self._chat_allowed(chat_id, thread_id, parent_chat_id):
|
|
2942
|
+
return
|
|
2943
|
+
if self._is_duplicate(key):
|
|
2944
|
+
return
|
|
2768
2945
|
target_text = str((target or {}).get("message") or "") or None
|
|
2769
2946
|
target_author = str((target or {}).get("fromId") or "") or None
|
|
2770
2947
|
target_is_own = bool(self._me_id and target_author == self._me_id)
|
|
@@ -2777,6 +2954,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2777
2954
|
chat_type=chat_type,
|
|
2778
2955
|
user_id=user_id,
|
|
2779
2956
|
user_name=_inline_sender_identity(_inline_sender_profile(event))[0] or None,
|
|
2957
|
+
thread_id=thread_id,
|
|
2958
|
+
parent_chat_id=parent_chat_id,
|
|
2780
2959
|
message_id=key,
|
|
2781
2960
|
)
|
|
2782
2961
|
await self.handle_message(MessageEvent(
|
|
@@ -2813,7 +2992,13 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2813
2992
|
text = "message:history_cleared"
|
|
2814
2993
|
if not chat_id or not text:
|
|
2815
2994
|
return
|
|
2816
|
-
if self._group_policy == "disabled":
|
|
2995
|
+
if self._group_policy == "disabled" or (user_id and not self._allowed("group", user_id)):
|
|
2996
|
+
return
|
|
2997
|
+
scope = await self._message_scope(chat_id, {}, required=True)
|
|
2998
|
+
if scope is None:
|
|
2999
|
+
return
|
|
3000
|
+
_, thread_id, parent_chat_id = scope
|
|
3001
|
+
if not self._chat_allowed(chat_id, thread_id, parent_chat_id):
|
|
2817
3002
|
return
|
|
2818
3003
|
key = f"{kind}:{chat_id}:{user_id}:{event.get('seq') or ''}:{text}"
|
|
2819
3004
|
if self._is_duplicate(key):
|
|
@@ -2824,8 +3009,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2824
3009
|
chat_type="group",
|
|
2825
3010
|
user_id=user_id or None,
|
|
2826
3011
|
user_name=_inline_sender_identity(_inline_sender_profile(event))[0] or None,
|
|
3012
|
+
thread_id=thread_id,
|
|
3013
|
+
parent_chat_id=parent_chat_id,
|
|
2827
3014
|
message_id=key,
|
|
2828
3015
|
)
|
|
3016
|
+
# Delete/history events may have no actor. Preserve the host's own
|
|
3017
|
+
# authorization of these events rather than inventing a sender grant.
|
|
2829
3018
|
await self.handle_message(MessageEvent(
|
|
2830
3019
|
text=text,
|
|
2831
3020
|
message_type=MessageType.TEXT,
|
|
@@ -3665,7 +3854,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3665
3854
|
return None
|
|
3666
3855
|
return str(reply_to)
|
|
3667
3856
|
|
|
3668
|
-
async def _get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
|
3857
|
+
async def _get_chat_info(self, chat_id: str, *, required: bool = False) -> Dict[str, Any]:
|
|
3669
3858
|
target = _target_from_chat_id(chat_id)
|
|
3670
3859
|
normalized = str(target.get("chatId") or "").strip()
|
|
3671
3860
|
if not normalized:
|
|
@@ -3679,7 +3868,16 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3679
3868
|
data = await self._sidecar_call("/chat", {"target": {"chatId": normalized}})
|
|
3680
3869
|
result = data.get("result") if isinstance(data, dict) else None
|
|
3681
3870
|
info = result if isinstance(result, dict) else {}
|
|
3682
|
-
except Exception:
|
|
3871
|
+
except Exception as exc:
|
|
3872
|
+
if required:
|
|
3873
|
+
if isinstance(exc, InlineSidecarError) and exc.error_kind in {"forbidden", "not_found"}:
|
|
3874
|
+
raise
|
|
3875
|
+
raise InlineInboundDeferred("chat metadata request failed") from exc
|
|
3876
|
+
return {}
|
|
3877
|
+
resolved_id = self._chat_info_id(info, "id")
|
|
3878
|
+
if not info or (resolved_id is not None and resolved_id != normalized):
|
|
3879
|
+
if required:
|
|
3880
|
+
raise InlineInboundDeferred("chat metadata unavailable or mismatched")
|
|
3683
3881
|
return {}
|
|
3684
3882
|
self._chat_info_cache[normalized] = (now, info)
|
|
3685
3883
|
self._chat_info_cache.move_to_end(normalized)
|
|
@@ -3779,12 +3977,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3779
3977
|
return self._id_allowed(self._group_allow_from, from_id)
|
|
3780
3978
|
return True
|
|
3781
3979
|
|
|
3782
|
-
async def _fetch_message(self, chat_id: str, message_id: str) -> Optional[Dict[str, Any]]:
|
|
3980
|
+
async def _fetch_message(self, chat_id: str, message_id: str, *, required: bool = False) -> Optional[Dict[str, Any]]:
|
|
3783
3981
|
try:
|
|
3784
3982
|
data = await self._sidecar_call("/messages", {"target": _target_from_chat_id(chat_id), "messageIds": [message_id]})
|
|
3785
3983
|
messages = (data.get("result") or {}).get("messages") or []
|
|
3786
3984
|
return messages[0] if messages else None
|
|
3787
|
-
except Exception:
|
|
3985
|
+
except Exception as exc:
|
|
3986
|
+
if required and not (isinstance(exc, InlineSidecarError) and exc.error_kind in {"forbidden", "not_found"}):
|
|
3987
|
+
raise InlineInboundDeferred("action target temporarily unavailable") from exc
|
|
3788
3988
|
return None
|
|
3789
3989
|
|
|
3790
3990
|
async def _handle_action(self, event: Dict[str, Any]) -> bool:
|
|
@@ -3825,43 +4025,117 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3825
4025
|
async def _action_allowed(self, event: Dict[str, Any]) -> bool:
|
|
3826
4026
|
actor_id = str(event.get("actorUserId") or "").strip()
|
|
3827
4027
|
interaction_id = str(event.get("interactionId") or "")
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
await self.
|
|
3832
|
-
|
|
4028
|
+
chat_id = str(event.get("chatId") or "")
|
|
4029
|
+
message_id = str(event.get("messageId") or "")
|
|
4030
|
+
msg = await self._fetch_message(chat_id, message_id) if chat_id and message_id else None
|
|
4031
|
+
scope = await self._message_scope(chat_id, msg) if msg else None
|
|
4032
|
+
verdict = None
|
|
4033
|
+
if scope is not None:
|
|
4034
|
+
chat_type, thread_id, parent_chat_id = scope
|
|
4035
|
+
verdict = self._actor_authorization(
|
|
4036
|
+
chat_type, actor_id, chat_id, thread_id=thread_id, parent_chat_id=parent_chat_id,
|
|
4037
|
+
is_bot=_inline_sender_profile(event).get("bot") is True,
|
|
4038
|
+
)
|
|
4039
|
+
if verdict is True:
|
|
4040
|
+
return True
|
|
4041
|
+
await self._answer_action(interaction_id, "Access check temporarily unavailable. Try again." if verdict is None else "Not authorized")
|
|
4042
|
+
logger.info("[inline] blocked action actor=%s chat=%s action=%s", actor_id or "unknown", chat_id or "unknown", event.get("actionId") or "")
|
|
3833
4043
|
return False
|
|
3834
4044
|
|
|
3835
|
-
def
|
|
3836
|
-
|
|
4045
|
+
async def _message_scope(
|
|
4046
|
+
self, chat_id: str, msg: Dict[str, Any], *, required: bool = False,
|
|
4047
|
+
) -> Optional[tuple[str, Optional[str], Optional[str]]]:
|
|
4048
|
+
"""Resolve the same parent/thread identity used by normal message intake."""
|
|
4049
|
+
chat_type = self._chat_type_from_message(msg)
|
|
4050
|
+
thread_id = self._thread_id_from_message(msg)
|
|
4051
|
+
parent_chat_id = self._parent_chat_id_from_message(msg)
|
|
4052
|
+
if chat_type == "group" and not thread_id:
|
|
4053
|
+
if not parent_chat_id:
|
|
4054
|
+
try:
|
|
4055
|
+
info = await asyncio.wait_for(
|
|
4056
|
+
self._get_chat_info(chat_id, required=required),
|
|
4057
|
+
timeout=_INBOUND_ACCESS_LOOKUP_TIMEOUT_SECONDS if required else _CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS,
|
|
4058
|
+
)
|
|
4059
|
+
except asyncio.TimeoutError as exc:
|
|
4060
|
+
if required:
|
|
4061
|
+
raise InlineInboundDeferred("chat metadata timed out") from exc
|
|
4062
|
+
return None
|
|
4063
|
+
if not info:
|
|
4064
|
+
if required:
|
|
4065
|
+
raise InlineInboundDeferred("chat metadata unavailable")
|
|
4066
|
+
return None
|
|
4067
|
+
parent_chat_id = self._chat_info_id(info, "parentChatId")
|
|
4068
|
+
if parent_chat_id:
|
|
4069
|
+
thread_id = chat_id
|
|
4070
|
+
return chat_type, thread_id, parent_chat_id
|
|
4071
|
+
|
|
4072
|
+
async def _chat_actor_authorized(self, chat_id: str, actor_id: str) -> Optional[bool]:
|
|
4073
|
+
"""Check the actual target when a child control changes parent-wide state."""
|
|
4074
|
+
try:
|
|
4075
|
+
info = await asyncio.wait_for(
|
|
4076
|
+
self._get_chat_info(chat_id), timeout=_CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS,
|
|
4077
|
+
)
|
|
4078
|
+
except asyncio.TimeoutError:
|
|
4079
|
+
return None
|
|
4080
|
+
if not info:
|
|
4081
|
+
return None
|
|
4082
|
+
parent_chat_id = self._chat_info_id(info, "parentChatId")
|
|
4083
|
+
return self._actor_authorization(
|
|
4084
|
+
self._bot_settings_chat_type(info), actor_id, chat_id,
|
|
4085
|
+
thread_id=chat_id if parent_chat_id else None, parent_chat_id=parent_chat_id,
|
|
4086
|
+
)
|
|
4087
|
+
|
|
4088
|
+
def _actor_authorized(
|
|
4089
|
+
self, chat_type: Optional[str], actor_id: str, chat_id: Optional[str] = None, *,
|
|
4090
|
+
thread_id: Optional[str] = None, parent_chat_id: Optional[str] = None, is_bot: bool = False,
|
|
4091
|
+
) -> bool:
|
|
4092
|
+
return self._actor_authorization(
|
|
4093
|
+
chat_type, actor_id, chat_id, thread_id=thread_id,
|
|
4094
|
+
parent_chat_id=parent_chat_id, is_bot=is_bot,
|
|
4095
|
+
) is True
|
|
4096
|
+
|
|
4097
|
+
def _actor_authorization(
|
|
4098
|
+
self, chat_type: Optional[str], actor_id: str, chat_id: Optional[str] = None, *,
|
|
4099
|
+
thread_id: Optional[str] = None, parent_chat_id: Optional[str] = None, is_bot: bool = False,
|
|
4100
|
+
) -> Optional[bool]:
|
|
4101
|
+
# The host may trust an adapter's allowlist policy on the assumption that
|
|
4102
|
+
# intake already checked it. Always enforce our restrictions before asking.
|
|
4103
|
+
if not actor_id or chat_type not in {"dm", "group"} or not self._allowed(chat_type, actor_id):
|
|
3837
4104
|
return False
|
|
4105
|
+
if chat_type == "group" and not self._chat_allowed(chat_id or "", thread_id, parent_chat_id):
|
|
4106
|
+
return False
|
|
4107
|
+
if getattr(self, "_authorization_check", None) is not None:
|
|
4108
|
+
# Hermes owns pairing, profile routing, and runtime authorization.
|
|
4109
|
+
# Unknown/error is unavailable, never a reason to bypass a wired checker.
|
|
4110
|
+
runner = getattr(self, "gateway_runner", None)
|
|
4111
|
+
if parent_chat_id and runner is not None:
|
|
4112
|
+
# The three-argument native callback drops parent_chat_id. Inline
|
|
4113
|
+
# child chats need it for parent-routed profiles, so ask the same
|
|
4114
|
+
# host predicate with the complete source, retaining transport identity.
|
|
4115
|
+
try:
|
|
4116
|
+
source = self.build_source(
|
|
4117
|
+
chat_id=chat_id or "", chat_type=chat_type, user_id=actor_id,
|
|
4118
|
+
thread_id=thread_id, parent_chat_id=parent_chat_id, is_bot=is_bot,
|
|
4119
|
+
)
|
|
4120
|
+
if getattr(source, "profile_route_rejected", False):
|
|
4121
|
+
return False
|
|
4122
|
+
verdict = runner._is_user_authorized_for_source(source)
|
|
4123
|
+
return verdict if isinstance(verdict, bool) else None
|
|
4124
|
+
except Exception:
|
|
4125
|
+
logger.warning("[inline] thread authorization unavailable", exc_info=True)
|
|
4126
|
+
return None
|
|
4127
|
+
return self._is_sender_authorized(
|
|
4128
|
+
actor_id, chat_type, chat_id, is_bot=is_bot, thread_id=thread_id,
|
|
4129
|
+
)
|
|
4130
|
+
# Standalone adapters have no runner. Only explicit grants are usable;
|
|
4131
|
+
# an open intake policy alone must not authorize credentialed side effects.
|
|
3838
4132
|
if self._allow_all or _truthy(os.getenv("GATEWAY_ALLOW_ALL_USERS"), False):
|
|
3839
4133
|
return True
|
|
3840
4134
|
if self._id_allowed(self._parse_id_set(os.getenv("GATEWAY_ALLOWED_USERS")), actor_id):
|
|
3841
4135
|
return True
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
if self._dm_policy == "allowlist" or self._allow_from:
|
|
3846
|
-
return self._id_allowed(self._allow_from, actor_id)
|
|
3847
|
-
return False
|
|
3848
|
-
if self._group_policy == "disabled":
|
|
3849
|
-
return False
|
|
3850
|
-
if self._id_allowed(self._group_allow_from, actor_id):
|
|
3851
|
-
return True
|
|
3852
|
-
if self._id_allowed(self._allow_from, actor_id):
|
|
3853
|
-
return True
|
|
3854
|
-
return False
|
|
3855
|
-
|
|
3856
|
-
async def _action_chat_type(self, event: Dict[str, Any]) -> Optional[str]:
|
|
3857
|
-
chat_id = str(event.get("chatId") or "")
|
|
3858
|
-
message_id = str(event.get("messageId") or "")
|
|
3859
|
-
if not chat_id or not message_id:
|
|
3860
|
-
return None
|
|
3861
|
-
msg = await self._fetch_message(chat_id, message_id)
|
|
3862
|
-
if not msg:
|
|
3863
|
-
return None
|
|
3864
|
-
return self._chat_type_from_message(msg)
|
|
4136
|
+
return self._id_allowed(self._allow_from, actor_id) or (
|
|
4137
|
+
chat_type == "group" and self._id_allowed(self._group_allow_from, actor_id)
|
|
4138
|
+
)
|
|
3865
4139
|
|
|
3866
4140
|
async def _handle_clarify_action(self, event: Dict[str, Any]) -> bool:
|
|
3867
4141
|
action_id = str(event.get("actionId") or "")
|
|
@@ -3990,7 +4264,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3990
4264
|
if not target_chat_id:
|
|
3991
4265
|
await self._answer_action(interaction_id, "Thread controls expired")
|
|
3992
4266
|
return True
|
|
3993
|
-
if not await self.
|
|
4267
|
+
if not await self._action_allowed(event):
|
|
4268
|
+
return True
|
|
4269
|
+
if target_chat_id != chat_id and not await self._chat_actor_authorized(
|
|
4270
|
+
target_chat_id, str(event.get("actorUserId") or ""),
|
|
4271
|
+
):
|
|
4272
|
+
await self._answer_action(interaction_id, "Parent chat access could not be confirmed. Check access and try again.")
|
|
3994
4273
|
return True
|
|
3995
4274
|
try:
|
|
3996
4275
|
if choice == "reset":
|
|
@@ -4012,24 +4291,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4012
4291
|
await self._answer_action(interaction_id, "Thread setting failed")
|
|
4013
4292
|
return True
|
|
4014
4293
|
|
|
4015
|
-
async def _thread_action_allowed(self, event: Dict[str, Any], state: Dict[str, Any]) -> bool:
|
|
4016
|
-
actor_id = str(event.get("actorUserId") or "").strip()
|
|
4017
|
-
interaction_id = str(event.get("interactionId") or "")
|
|
4018
|
-
chat_type = await self._action_chat_type(event)
|
|
4019
|
-
if self._actor_authorized(chat_type, actor_id):
|
|
4020
|
-
return True
|
|
4021
|
-
display_chat_id = self._chat_key(state.get("display_chat_id"))
|
|
4022
|
-
target_chat_id = self._chat_key(state.get("target_chat_id"))
|
|
4023
|
-
if chat_type and actor_id and self._allowed(chat_type, actor_id):
|
|
4024
|
-
if chat_type == "dm":
|
|
4025
|
-
return True
|
|
4026
|
-
thread_id = display_chat_id if target_chat_id and display_chat_id != target_chat_id else None
|
|
4027
|
-
if self._chat_allowed(display_chat_id or str(event.get("chatId") or ""), thread_id, target_chat_id):
|
|
4028
|
-
return True
|
|
4029
|
-
await self._answer_action(interaction_id, "Not authorized")
|
|
4030
|
-
logger.info("[inline] blocked thread action actor=%s chat_type=%s action=%s", actor_id or "unknown", chat_type or "unknown", event.get("actionId") or "")
|
|
4031
|
-
return False
|
|
4032
|
-
|
|
4033
4294
|
@staticmethod
|
|
4034
4295
|
def _is_model_picker_action(action_id: str) -> bool:
|
|
4035
4296
|
return action_id.startswith(("mp:", "mpg:", "mm:", "mc:", "mg:", "mb:", "mx:"))
|
package/plugin/inline/cli.py
CHANGED
|
@@ -27,7 +27,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
|
27
27
|
_MAX_TOKEN_BYTES = 16 * 1024
|
|
28
28
|
_MAX_PROBE_RESPONSE_BYTES = 64 * 1024
|
|
29
29
|
_MACHINE_SETUP_PROTOCOL_VERSION = 1
|
|
30
|
-
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.
|
|
30
|
+
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.22-alpha.0"
|
|
31
31
|
_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
32
32
|
|
|
33
33
|
|
|
@@ -514,15 +514,17 @@ def _compatibility_status() -> dict:
|
|
|
514
514
|
return {"ok": False, "reason": "plugin_load_failed"}
|
|
515
515
|
if "inline" not in loaded.tools_registered:
|
|
516
516
|
return {"ok": False, "reason": "tool_not_registered"}
|
|
517
|
-
#
|
|
518
|
-
#
|
|
517
|
+
# The moved-import scanner is optional: hosts may predate it or retain
|
|
518
|
+
# only updater stubs after removing the compat layer. Actual loading and
|
|
519
|
+
# this installation's tool registration above remain mandatory.
|
|
519
520
|
try:
|
|
520
|
-
|
|
521
|
+
import hermes_cli.plugin_compat as plugin_compat
|
|
521
522
|
except ModuleNotFoundError as error:
|
|
522
523
|
if error.name != "hermes_cli.plugin_compat":
|
|
523
524
|
raise
|
|
524
525
|
else:
|
|
525
|
-
|
|
526
|
+
plugin_hits = getattr(plugin_compat, "plugin_hits", None)
|
|
527
|
+
if plugin_hits is not None and plugin_hits(loaded.manifest):
|
|
526
528
|
return {"ok": False, "reason": "deprecated_imports"}
|
|
527
529
|
return {"ok": True, "reason": "loaded", "pluginPath": str(plugin_dir)}
|
|
528
530
|
except Exception:
|
|
@@ -5599,10 +5599,13 @@ var require_websocket_server = __commonJS(function(exports, module) {
|
|
|
5599
5599
|
import http from "node:http";
|
|
5600
5600
|
|
|
5601
5601
|
// src/sidecar/inbound-stream.ts
|
|
5602
|
+
import { randomUUID } from "node:crypto";
|
|
5603
|
+
|
|
5602
5604
|
class InboundStream {
|
|
5603
5605
|
consumer = null;
|
|
5604
5606
|
stopped = false;
|
|
5605
5607
|
changed = new Set;
|
|
5608
|
+
pending = new Map;
|
|
5606
5609
|
attach(consumer) {
|
|
5607
5610
|
if (this.stopped) {
|
|
5608
5611
|
consumer.end();
|
|
@@ -5621,6 +5624,13 @@ class InboundStream {
|
|
|
5621
5624
|
this.wake();
|
|
5622
5625
|
previous?.end();
|
|
5623
5626
|
}
|
|
5627
|
+
acknowledge(deliveryId) {
|
|
5628
|
+
const receipt = this.pending.get(deliveryId);
|
|
5629
|
+
if (!receipt)
|
|
5630
|
+
return;
|
|
5631
|
+
receipt.acknowledged = true;
|
|
5632
|
+
this.wake();
|
|
5633
|
+
}
|
|
5624
5634
|
close() {
|
|
5625
5635
|
this.stopped = true;
|
|
5626
5636
|
const previous = this.consumer;
|
|
@@ -5629,54 +5639,41 @@ class InboundStream {
|
|
|
5629
5639
|
previous?.end();
|
|
5630
5640
|
}
|
|
5631
5641
|
async deliver(event) {
|
|
5632
|
-
|
|
5642
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) {
|
|
5643
|
+
throw new Error("Inbound delivery requires an event object");
|
|
5644
|
+
}
|
|
5645
|
+
const deliveryId = randomUUID();
|
|
5646
|
+
const line = JSON.stringify({ ...event, _inlineDeliveryId: deliveryId }) + `
|
|
5633
5647
|
`;
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
owner.once("close", onChange);
|
|
5656
|
-
owner.once("error", onChange);
|
|
5657
|
-
this.changed.add(onChange);
|
|
5658
|
-
});
|
|
5659
|
-
try {
|
|
5660
|
-
if (owner.destroyed || owner.writableEnded) {
|
|
5661
|
-
if (this.consumer === owner)
|
|
5662
|
-
this.consumer = null;
|
|
5648
|
+
const receipt = { acknowledged: false };
|
|
5649
|
+
this.pending.set(deliveryId, receipt);
|
|
5650
|
+
let writtenTo = null;
|
|
5651
|
+
try {
|
|
5652
|
+
while (!this.stopped) {
|
|
5653
|
+
if (receipt.acknowledged)
|
|
5654
|
+
return;
|
|
5655
|
+
const owner = this.consumer;
|
|
5656
|
+
if (owner && owner !== writtenTo) {
|
|
5657
|
+
try {
|
|
5658
|
+
if (owner.destroyed || owner.writableEnded) {
|
|
5659
|
+
if (this.consumer === owner)
|
|
5660
|
+
this.consumer = null;
|
|
5661
|
+
continue;
|
|
5662
|
+
}
|
|
5663
|
+
writtenTo = owner;
|
|
5664
|
+
owner.write(line);
|
|
5665
|
+
} catch {
|
|
5666
|
+
if (this.consumer === owner)
|
|
5667
|
+
this.consumer = null;
|
|
5668
|
+
}
|
|
5663
5669
|
continue;
|
|
5664
5670
|
}
|
|
5665
|
-
|
|
5666
|
-
if (this.consumer !== owner || this.stopped)
|
|
5667
|
-
continue;
|
|
5668
|
-
if (accepted)
|
|
5669
|
-
return;
|
|
5670
|
-
if (await drained && this.consumer === owner && !this.stopped)
|
|
5671
|
-
return;
|
|
5672
|
-
} catch {
|
|
5673
|
-
if (this.consumer === owner)
|
|
5674
|
-
this.consumer = null;
|
|
5675
|
-
} finally {
|
|
5676
|
-
cleanup();
|
|
5671
|
+
await new Promise((resolve) => this.changed.add(resolve));
|
|
5677
5672
|
}
|
|
5673
|
+
throw new Error("Inbound stream closed before delivery completed");
|
|
5674
|
+
} finally {
|
|
5675
|
+
this.pending.delete(deliveryId);
|
|
5678
5676
|
}
|
|
5679
|
-
throw new Error("Inbound stream closed before delivery completed");
|
|
5680
5677
|
}
|
|
5681
5678
|
wake() {
|
|
5682
5679
|
for (const resolve of this.changed)
|
|
@@ -5705,6 +5702,13 @@ var botSettingsEventKinds = new Set([
|
|
|
5705
5702
|
function inboundEventNeedsSenderResolution(event) {
|
|
5706
5703
|
return !botSettingsEventKinds.has(event.kind ?? "");
|
|
5707
5704
|
}
|
|
5705
|
+
function inboundEventIsServiceMessage(event) {
|
|
5706
|
+
if (event.kind !== "message.new" && event.kind !== "message.edit")
|
|
5707
|
+
return false;
|
|
5708
|
+
const message = asOptionalRecord(event.message);
|
|
5709
|
+
const raw = asOptionalRecord(message?.raw);
|
|
5710
|
+
return Boolean(asOptionalRecord(message?.serviceMessage) || asOptionalRecord(raw?.serviceMessage));
|
|
5711
|
+
}
|
|
5708
5712
|
|
|
5709
5713
|
class SidecarError extends Error {
|
|
5710
5714
|
errorKind;
|
|
@@ -5826,6 +5830,7 @@ function normalizeMessage(message) {
|
|
|
5826
5830
|
replies: message.replies ?? null,
|
|
5827
5831
|
actions: message.actions ?? null,
|
|
5828
5832
|
rev: message.rev ?? null,
|
|
5833
|
+
...asOptionalRecord(message.serviceMessage) ? { serviceMessage: message.serviceMessage } : {},
|
|
5829
5834
|
raw: message
|
|
5830
5835
|
};
|
|
5831
5836
|
}
|
|
@@ -6024,6 +6029,8 @@ function explicitlyMentionsSelf(event, meId) {
|
|
|
6024
6029
|
return message?.entities?.entities?.some((e) => e.entity?.oneofKind === "mention" && e.entity.mention?.userId?.toString() === meId) ?? false;
|
|
6025
6030
|
}
|
|
6026
6031
|
async function deliverInboundEvent(event, owner) {
|
|
6032
|
+
if (inboundEventIsServiceMessage(event))
|
|
6033
|
+
return;
|
|
6027
6034
|
const explicitMention = explicitlyMentionsSelf(event, owner.meId);
|
|
6028
6035
|
while (!owner.signal.aborted) {
|
|
6029
6036
|
const resolution = explicitMention ? { provenanceVerified: false } : inboundEventNeedsSenderResolution(event) ? await owner.resolveSender(event) : { provenanceVerified: true };
|
|
@@ -46757,7 +46764,7 @@ function readUsers(result, kind) {
|
|
|
46757
46764
|
}
|
|
46758
46765
|
|
|
46759
46766
|
// src/sidecar/telemetry.ts
|
|
46760
|
-
import { randomUUID } from "node:crypto";
|
|
46767
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
46761
46768
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
46762
46769
|
var TELEMETRY_TIMEOUT_MS = 2000;
|
|
46763
46770
|
var TELEMETRY_DEDUP_MS = 5 * 60000;
|
|
@@ -46818,7 +46825,7 @@ function buildHermesTelemetryEvent(operation, error, options = {}) {
|
|
|
46818
46825
|
const exception = error instanceof Error ? error : new Error(String(error ?? "Unknown error"));
|
|
46819
46826
|
const frames = parseStack(exception, env);
|
|
46820
46827
|
return {
|
|
46821
|
-
event_id:
|
|
46828
|
+
event_id: randomUUID2().replaceAll("-", ""),
|
|
46822
46829
|
timestamp: new Date().toISOString(),
|
|
46823
46830
|
platform: "javascript",
|
|
46824
46831
|
level: "error",
|
|
@@ -47090,6 +47097,13 @@ async function handleRequest(req, res) {
|
|
|
47090
47097
|
writeJson(res, 405, { ok: false, error: "method not allowed", errorKind: "bad_format" });
|
|
47091
47098
|
return;
|
|
47092
47099
|
}
|
|
47100
|
+
if (url.pathname === "/inbound/ack") {
|
|
47101
|
+
const body2 = asRecord(await readJsonBody(req));
|
|
47102
|
+
const deliveryId = readRequiredString(body2, "deliveryId");
|
|
47103
|
+
inboundStream.acknowledge(deliveryId);
|
|
47104
|
+
writeJson(res, 200, { ok: true, result: {} });
|
|
47105
|
+
return;
|
|
47106
|
+
}
|
|
47093
47107
|
if (!connected && url.pathname !== "/shutdown") {
|
|
47094
47108
|
writeJson(res, 503, {
|
|
47095
47109
|
ok: false,
|