@inline-chat/hermes-agent-adapter 0.0.12 → 0.0.14

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 CHANGED
@@ -147,7 +147,7 @@ uv run ./hermes plugins list --plain --no-bundled
147
147
  Expected local output includes:
148
148
 
149
149
  ```text
150
- enabled user 0.0.12 inline-platform
150
+ enabled user 0.0.14 inline-platform
151
151
  ```
152
152
 
153
153
  ## Update Or Reinstall
@@ -170,10 +170,11 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
170
170
 
171
171
  - Hermes Agent: requires the external user plugin registry and native platform
172
172
  plugin loader available in Hermes Agent `0.17.x`. This package was validated
173
- against Hermes Agent `0.20.6` from source commit `31e41eed`.
173
+ against Hermes Agent `0.21.0` from source commit `29112bef` (tag
174
+ `v2026.8.31`).
174
175
  - Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
175
176
  system Node, or an explicit `INLINE_NODE_BIN` path all work.
176
- - Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.16` and is
177
+ - Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.17` and is
177
178
  bundled into the npm package, so Hermes startup does not run `npm install`.
178
179
  - Live sends require a valid Inline user or bot token in `INLINE_TOKEN`,
179
180
  `INLINE_BOT_TOKEN`, `platforms.inline.token`, or `inline.token`.
@@ -313,6 +314,8 @@ Access control follows Hermes' native platform model:
313
314
  | `INLINE_CONNECT_RETRY_MAX_MS` | Maximum sidecar retry delay after repeated realtime startup failures. Defaults to `15000`. |
314
315
  | `INLINE_SIDECAR_PORT` | Fixed loopback port for the sidecar. Must be `1` through `65535`. Defaults to `8794`; `test-send` uses a random free port. |
315
316
  | `INLINE_SIDECAR_BIND` | Sidecar bind host. Must be loopback: `127.0.0.1`, `localhost`, or `::1`. Defaults to `127.0.0.1`. |
317
+ | `INLINE_HERMES_SENTRY_DSN` | Explicitly enables adapter and sidecar error reporting to the configured collector. Reporting is disabled when unset. |
318
+ | `INLINE_PLUGIN_TELEMETRY` | Set to `off`, `0`, or `false` to disable explicitly configured plugin error reporting. `DO_NOT_TRACK=1` is also honored. |
316
319
  | `platforms.inline.typing_indicator` | Hermes-native toggle for Inline typing/presence while a turn is running. Defaults to `true`; set to `false` to keep busy threads visually quiet. |
317
320
  | `platforms.inline.gateway_restart_notification` | Hermes-native toggle for gateway online/restarted notices. Defaults to `true`. |
318
321
 
@@ -399,6 +402,28 @@ the normal reply.
399
402
  The plugin id is `inline`, which is intentionally the same id an eventual
400
403
  bundled Hermes adapter should use.
401
404
 
405
+ ## Error Reporting And Privacy
406
+
407
+ The Inline adapter and its supervised sidecar keep error reporting disabled
408
+ unless the operator sets `INLINE_HERMES_SENTRY_DSN` to an explicit collector.
409
+ When enabled, reports include the raw exception type and message, traceback
410
+ paths, line/function locations, plugin release,
411
+ operation name, runtime, OS, and architecture so maintainers can diagnose
412
+ failures in subsequent releases.
413
+
414
+ Reports do not attach Inline or Hermes message events, request bodies,
415
+ user/chat/account identifiers, breadcrumbs, source context, or stack locals.
416
+ Known token, password, authorization, sidecar credential, and secret-shaped
417
+ values are redacted before upload. Inline's Sentry project also enables default
418
+ server-side data scrubbing and IP-address scrubbing. Because dependency
419
+ exception messages are preserved for diagnosis, they can still contain values
420
+ the dependency itself chose to place in an error.
421
+
422
+ Remove `INLINE_HERMES_SENTRY_DSN`, set `INLINE_PLUGIN_TELEMETRY=off`, or set
423
+ `DO_NOT_TRACK=1` to disable both adapter and sidecar reporting. Reporting is
424
+ best-effort, has a two-second network deadline, and never changes plugin success
425
+ or failure behavior.
426
+
402
427
  ## Troubleshooting
403
428
 
404
429
  Run `inline-hermes doctor --json` first. It checks the installed plugin path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/hermes-agent-adapter",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
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",
@@ -33,6 +33,7 @@
33
33
  "plugin/inline/cli.py",
34
34
  "plugin/inline/message_actions.py",
35
35
  "plugin/inline/plugin.yaml",
36
+ "plugin/inline/telemetry.py",
36
37
  "plugin/inline/tools.py",
37
38
  "plugin/inline/sidecar/index.mjs"
38
39
  ],
@@ -50,7 +51,7 @@
50
51
  },
51
52
  "scripts": {
52
53
  "build": "mkdir -p plugin/inline/sidecar && bun build ./src/install.ts --outdir dist --entry-naming '[name].js' --target=node --format=esm --packages=bundle && bun build ./src/sidecar/index.ts --outdir plugin/inline/sidecar --entry-naming 'index.mjs' --target=node --format=esm --packages=bundle && tsc -p tsconfig.json --emitDeclarationOnly",
53
- "lint": "bunx oxlint --ignore-path ../.oxlintignore scripts/release-stage.mjs src/install.ts src/sidecar/contract.ts src/sidecar/index.ts src/sidecar/user-directory.ts src/sidecar/user-directory.test.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts vitest.config.ts",
54
+ "lint": "bunx oxlint --ignore-path ../.oxlintignore scripts/release-stage.mjs src/install.ts src/sidecar/contract.ts src/sidecar/index.ts src/sidecar/telemetry.ts src/sidecar/telemetry.test.ts src/sidecar/user-directory.ts src/sidecar/user-directory.test.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts tests/telemetry-python.test.ts vitest.config.ts",
54
55
  "pretest": "bun run build",
55
56
  "prepack": "bun run build",
56
57
  "prepublishOnly": "bun run check",
@@ -61,7 +62,7 @@
61
62
  "check": "bun run typecheck && bun run lint && bun run test"
62
63
  },
63
64
  "dependencies": {
64
- "@inline-chat/realtime-sdk": "0.0.16",
65
+ "@inline-chat/realtime-sdk": "0.0.17",
65
66
  "yaml": "2.9.0"
66
67
  },
67
68
  "devDependencies": {
@@ -81,7 +82,7 @@
81
82
  },
82
83
  "machineSetupProtocol": 1,
83
84
  "minHermesVersion": "0.17.0",
84
- "testedHermesVersion": "0.20.6",
85
- "testedHermesCommit": "31e41eed"
85
+ "testedHermesVersion": "0.21.0",
86
+ "testedHermesCommit": "29112bef"
86
87
  }
87
88
  }
@@ -55,6 +55,7 @@ from .message_actions import (
55
55
  parse_inline_agent_action_reply_target,
56
56
  resolve_inline_message_action_ownership,
57
57
  )
58
+ from .telemetry import capture_plugin_error
58
59
 
59
60
  logger = logging.getLogger(__name__)
60
61
 
@@ -600,6 +601,36 @@ def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[Li
600
601
  return commands, hidden_count + hidden_local + skipped
601
602
 
602
603
 
604
+ def _inline_skill_catalog() -> List[Dict[str, Any]]:
605
+ """Return installed, enabled Hermes skills in the Bot API catalog shape."""
606
+ from tools.skills_tool import _find_all_skills, _sort_skills
607
+
608
+ published: List[Dict[str, Any]] = []
609
+ seen: set[str] = set()
610
+ for skill in _sort_skills(_find_all_skills(skip_disabled=False)):
611
+ if len(published) >= 250:
612
+ break
613
+ key = str(skill.get("name") or "").strip()
614
+ if not key or _utf16_length(key) > 256 or key in seen:
615
+ continue
616
+ # At most two UTF-16 code units per Python Unicode scalar keeps the
617
+ # 4,000-unit validation bound even for non-BMP descriptions.
618
+ description = str(skill.get("description") or "").strip()[:2000].rstrip()
619
+ seen.add(key)
620
+ published.append({
621
+ "key": key,
622
+ "name": key,
623
+ **({"description": description} if description else {}),
624
+ "sort_order": len(published),
625
+ })
626
+ return published
627
+
628
+
629
+ def _utf16_length(value: str) -> int:
630
+ """Match JavaScript and Bot API string-length validation."""
631
+ return len(value.encode("utf-16-le", errors="surrogatepass")) // 2
632
+
633
+
603
634
  def _token_value(raw: Any) -> str:
604
635
  if raw is None:
605
636
  return ""
@@ -971,6 +1002,18 @@ class InlineAdapter(BasePlatformAdapter):
971
1002
  self._context_backfill_seen: "OrderedDict[str, float]" = OrderedDict()
972
1003
  self._reply_thread_overrides = self._load_reply_thread_overrides()
973
1004
 
1005
+ def _report_error(self, operation: str, error: BaseException, *, handled: bool = True) -> None:
1006
+ try:
1007
+ capture_plugin_error(
1008
+ operation,
1009
+ error,
1010
+ handled=handled,
1011
+ secrets=(self._token, self._sidecar_token),
1012
+ )
1013
+ except Exception:
1014
+ # Observability must never alter adapter behavior.
1015
+ pass
1016
+
974
1017
  @staticmethod
975
1018
  def _parse_id_set(raw: Any) -> set[str]:
976
1019
  if raw is None:
@@ -1014,6 +1057,7 @@ class InlineAdapter(BasePlatformAdapter):
1014
1057
  except FileNotFoundError:
1015
1058
  return {}
1016
1059
  except Exception as exc:
1060
+ self._report_error("settings.load", exc)
1017
1061
  logger.warning("[inline] failed to load Inline adapter settings: %s", exc)
1018
1062
  return {}
1019
1063
  raw = data.get("reply_threads") if isinstance(data, dict) else None
@@ -1045,6 +1089,7 @@ class InlineAdapter(BasePlatformAdapter):
1045
1089
  tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1046
1090
  tmp_path.replace(self._settings_path)
1047
1091
  except Exception as exc:
1092
+ self._report_error("settings.save", exc)
1048
1093
  logger.warning("[inline] failed to save Inline adapter settings: %s", exc)
1049
1094
 
1050
1095
  def _reply_thread_mode_for_chat(self, chat_id: str, parent_chat_id: Optional[str] = None) -> str:
@@ -1393,6 +1438,7 @@ class InlineAdapter(BasePlatformAdapter):
1393
1438
  except asyncio.CancelledError:
1394
1439
  raise
1395
1440
  except Exception as exc:
1441
+ self._report_error("settings.answer", exc)
1396
1442
  logger.warning("[inline] agent settings answer expired: %s", exc)
1397
1443
  return False
1398
1444
 
@@ -1416,6 +1462,7 @@ class InlineAdapter(BasePlatformAdapter):
1416
1462
  context = await self._bot_settings_context(event)
1417
1463
  response = {"result": {"oneofKind": "document", "document": self._bot_settings_document(context)}}
1418
1464
  except Exception as exc:
1465
+ self._report_error("settings.load", exc)
1419
1466
  logger.warning("[inline] failed to load Hermes agent settings: %s", exc)
1420
1467
  response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_FAILED, "Hermes could not load settings.")
1421
1468
  answered = await self._answer_bot_settings(request_id, response)
@@ -1530,6 +1577,7 @@ class InlineAdapter(BasePlatformAdapter):
1530
1577
  except ValueError as exc:
1531
1578
  response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_INVALID_VALUE, str(exc).capitalize())
1532
1579
  except Exception as exc:
1580
+ self._report_error("settings.update", exc)
1533
1581
  logger.warning("[inline] failed to update Hermes agent settings: %s", exc)
1534
1582
  response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_FAILED, "Hermes could not update this setting.")
1535
1583
  answered = await self._answer_bot_settings(request_id, response)
@@ -1776,6 +1824,7 @@ class InlineAdapter(BasePlatformAdapter):
1776
1824
  try:
1777
1825
  await self._sidecar_call("/follow-mode", {"target": target, "mode": mode})
1778
1826
  except Exception as exc:
1827
+ self._report_error("follow_mode.update", exc)
1779
1828
  logger.warning("[inline] /%s failed for chat %s: %s", command, chat_id, exc)
1780
1829
  await self.send(
1781
1830
  chat_id,
@@ -1814,6 +1863,7 @@ class InlineAdapter(BasePlatformAdapter):
1814
1863
  try:
1815
1864
  await self._start_sidecar()
1816
1865
  except Exception as exc:
1866
+ self._report_error("sidecar.start", exc, handled=False)
1817
1867
  self._set_fatal_error("SIDECAR_FAILED", f"failed to start Inline sidecar: {exc}", retryable=True)
1818
1868
  await self._stop_sidecar()
1819
1869
  await self._http_client.aclose()
@@ -1965,8 +2015,6 @@ class InlineAdapter(BasePlatformAdapter):
1965
2015
  self._sidecar_supervisor_task = None
1966
2016
 
1967
2017
  def _schedule_bot_command_sync(self) -> None:
1968
- if not self._sync_commands:
1969
- return
1970
2018
  if self._command_sync_task is not None and not self._command_sync_task.done():
1971
2019
  return
1972
2020
  self._command_sync_task = asyncio.get_event_loop().create_task(self._run_bot_command_sync())
@@ -1974,9 +2022,11 @@ class InlineAdapter(BasePlatformAdapter):
1974
2022
  async def _run_bot_command_sync(self) -> None:
1975
2023
  try:
1976
2024
  await self._sync_bot_commands()
2025
+ await self._sync_bot_skills()
1977
2026
  except asyncio.CancelledError:
1978
2027
  raise
1979
2028
  except Exception as exc:
2029
+ self._report_error("commands_and_skills.sync", exc)
1980
2030
  logger.warning("[inline] bot command sync failed: %s", exc)
1981
2031
  finally:
1982
2032
  if asyncio.current_task() is self._command_sync_task:
@@ -1996,8 +2046,20 @@ class InlineAdapter(BasePlatformAdapter):
1996
2046
  hidden_suffix = f", {hidden_count} hidden" if hidden_count else ""
1997
2047
  logger.info("[inline] bot commands synced (%d command%s%s)", len(synced), "" if len(synced) == 1 else "s", hidden_suffix)
1998
2048
  except Exception as exc:
2049
+ self._report_error("commands.sync", exc)
1999
2050
  logger.warning("[inline] bot command sync failed: %s", exc)
2000
2051
 
2052
+ async def _sync_bot_skills(self) -> None:
2053
+ if self._http_client is None:
2054
+ return
2055
+ try:
2056
+ skills = _inline_skill_catalog()
2057
+ await self._call_bot_api("setMySkills", {"skills": skills})
2058
+ logger.info("[inline] bot skills synced (%d skill%s)", len(skills), "" if len(skills) == 1 else "s")
2059
+ except Exception as exc:
2060
+ self._report_error("skills.sync", exc)
2061
+ logger.warning("[inline] bot skill sync failed: %s", exc)
2062
+
2001
2063
  async def _set_bot_commands_with_retry(self, commands: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
2002
2064
  try:
2003
2065
  await self._call_bot_api("setMyCommands", {"commands": commands})
@@ -2087,6 +2149,7 @@ class InlineAdapter(BasePlatformAdapter):
2087
2149
  try:
2088
2150
  completed.result()
2089
2151
  except Exception as exc:
2152
+ self._report_error("settings.task", exc)
2090
2153
  logger.warning("[inline] agent settings task failed: %s", exc)
2091
2154
 
2092
2155
  task.add_done_callback(finished)
@@ -2114,6 +2177,7 @@ class InlineAdapter(BasePlatformAdapter):
2114
2177
  except Exception as exc:
2115
2178
  if not self._inbound_running:
2116
2179
  break
2180
+ self._report_error("inbound.stream", exc)
2117
2181
  logger.warning("[inline] inbound stream dropped (%s); reconnecting in %.1fs", exc, backoff)
2118
2182
  await asyncio.sleep(backoff)
2119
2183
  backoff = min(backoff * 2, 30.0)
@@ -2367,8 +2431,13 @@ class InlineAdapter(BasePlatformAdapter):
2367
2431
  agent_instructions = str(agent.get("instructions") or "").strip()
2368
2432
  agent_skill = str(agent.get("skillKey") or agent.get("skill_key") or "").strip()
2369
2433
  if agent_name:
2370
- specialization = agent_instructions or f'You are a specialized agent named "{agent_name}".'
2371
- channel_prompt = self._merge_channel_prompt(channel_prompt, specialization)
2434
+ if agent_instructions:
2435
+ channel_prompt = self._merge_channel_prompt(channel_prompt, agent_instructions)
2436
+ elif not agent_skill:
2437
+ channel_prompt = self._merge_channel_prompt(
2438
+ channel_prompt,
2439
+ f'You are a specialized agent named "{agent_name}". Proceed with the user\'s request.',
2440
+ )
2372
2441
  if agent_skill:
2373
2442
  auto_skill = list(dict.fromkeys([*(auto_skill or []), agent_skill]))
2374
2443
  except Exception as exc:
@@ -3749,7 +3818,8 @@ class InlineAdapter(BasePlatformAdapter):
3749
3818
  return True
3750
3819
  await self._finish_action(event, "Type your answer", "Type your answer:")
3751
3820
  return True
3752
- except Exception:
3821
+ except Exception as exc:
3822
+ self._report_error("action.clarify_other", exc)
3753
3823
  logger.exception("[inline] clarify other failed")
3754
3824
  return True
3755
3825
  try:
@@ -3767,7 +3837,8 @@ class InlineAdapter(BasePlatformAdapter):
3767
3837
  self._clarify_choices.pop(clarify_id, None)
3768
3838
  await self._finish_action(event, "Prompt expired", "Clarification expired.")
3769
3839
  return True
3770
- except Exception:
3840
+ except Exception as exc:
3841
+ self._report_error("action.clarify", exc)
3771
3842
  logger.exception("[inline] clarify action failed")
3772
3843
  return True
3773
3844
 
@@ -3800,7 +3871,8 @@ class InlineAdapter(BasePlatformAdapter):
3800
3871
  toast, text = labels[choice]
3801
3872
  await self._finish_action(event, toast, text)
3802
3873
  return True
3803
- except Exception:
3874
+ except Exception as exc:
3875
+ self._report_error("action.approval", exc)
3804
3876
  logger.exception("[inline] approval action failed")
3805
3877
  return True
3806
3878
 
@@ -3823,7 +3895,8 @@ class InlineAdapter(BasePlatformAdapter):
3823
3895
  text = str(result or ("Cancelled." if choice == "cancel" else "Recorded."))
3824
3896
  await self._finish_action(event, "Recorded", text)
3825
3897
  return True
3826
- except Exception:
3898
+ except Exception as exc:
3899
+ self._report_error("action.slash_confirm", exc)
3827
3900
  logger.exception("[inline] slash confirm action failed")
3828
3901
  return True
3829
3902
 
@@ -3867,7 +3940,8 @@ class InlineAdapter(BasePlatformAdapter):
3867
3940
  self._thread_action_sessions.pop(session_id, None)
3868
3941
  await self._answer_action(interaction_id, f"Reply threads: {mode}")
3869
3942
  return True
3870
- except Exception:
3943
+ except Exception as exc:
3944
+ self._report_error("action.thread", exc)
3871
3945
  logger.exception("[inline] thread action failed")
3872
3946
  await self._answer_action(interaction_id, "Thread setting failed")
3873
3947
  return True
@@ -3957,6 +4031,7 @@ class InlineAdapter(BasePlatformAdapter):
3957
4031
  result_text = await result_text
3958
4032
  result_text = str(result_text or "Selection applied.")
3959
4033
  except Exception as exc:
4034
+ self._report_error("action.choice_picker", exc)
3960
4035
  logger.error("[inline] choice picker selection failed (%s)", type(exc).__name__)
3961
4036
  result_text = "Selection failed; try again."
3962
4037
  failed = True
@@ -3995,7 +4070,8 @@ class InlineAdapter(BasePlatformAdapter):
3995
4070
  tmp_path = response_path.with_suffix(".tmp")
3996
4071
  tmp_path.write_text(answer, encoding="utf-8")
3997
4072
  tmp_path.replace(response_path)
3998
- except Exception:
4073
+ except Exception as exc:
4074
+ self._report_error("action.update_prompt", exc)
3999
4075
  logger.exception("[inline] failed to write Hermes update response")
4000
4076
  await self._answer_action(interaction_id, "Response failed; try again")
4001
4077
  return True
@@ -4171,6 +4247,7 @@ class InlineAdapter(BasePlatformAdapter):
4171
4247
  result_text = await result_text
4172
4248
  result_text = str(result_text or "Model switched.")
4173
4249
  except Exception as exc:
4250
+ self._report_error("action.model_picker", exc)
4174
4251
  logger.error("[inline] model picker switch failed (%s)", type(exc).__name__)
4175
4252
  result_text = "Model switch failed; try again."
4176
4253
  failed = True
@@ -4600,6 +4677,7 @@ class InlineAdapter(BasePlatformAdapter):
4600
4677
  result = data.get("result") or {}
4601
4678
  return str(result.get("chatId") or "") or None
4602
4679
  except Exception as exc:
4680
+ self._report_error("thread.create", exc)
4603
4681
  logger.debug("[inline] create handoff thread failed: %s", exc)
4604
4682
  return None
4605
4683
 
@@ -4779,7 +4857,7 @@ class InlineAdapter(BasePlatformAdapter):
4779
4857
  ]}]},
4780
4858
  })
4781
4859
  if not result.success:
4782
- # Hermes 0.20.6 treats any non-raising hook call as delivered and
4860
+ # Hermes treats any non-raising hook call as delivered and
4783
4861
  # otherwise suppresses its plaintext /approve and /deny fallback.
4784
4862
  raise RuntimeError(result.error or "failed to send Inline update prompt")
4785
4863
  self._remember(self._update_prompt_sessions, prompt_id, {
@@ -5011,6 +5089,8 @@ class InlineAdapter(BasePlatformAdapter):
5011
5089
  raw_response=result,
5012
5090
  )
5013
5091
  except InlineSidecarError as exc:
5092
+ if exc.error_kind == "unknown":
5093
+ self._report_error("sidecar.send", exc)
5014
5094
  return _send_result(
5015
5095
  success=False,
5016
5096
  error=str(exc),
@@ -5019,6 +5099,7 @@ class InlineAdapter(BasePlatformAdapter):
5019
5099
  error_kind=exc.error_kind,
5020
5100
  )
5021
5101
  except Exception as exc:
5102
+ self._report_error("sidecar.send", exc)
5022
5103
  return _send_result(success=False, error=str(exc), retryable=self._is_retryable_error(str(exc)))
5023
5104
 
5024
5105
  async def _sidecar_call(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
@@ -7,6 +7,7 @@ the plugin has already been discovered by Hermes.
7
7
  from __future__ import annotations
8
8
 
9
9
  import argparse
10
+ import hashlib
10
11
  import json
11
12
  import os
12
13
  import re
@@ -26,7 +27,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
26
27
  _MAX_TOKEN_BYTES = 16 * 1024
27
28
  _MAX_PROBE_RESPONSE_BYTES = 64 * 1024
28
29
  _MACHINE_SETUP_PROTOCOL_VERSION = 1
29
- _PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.12"
30
+ _PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.14"
30
31
  _ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
31
32
 
32
33
 
@@ -463,6 +464,7 @@ def _status(args) -> int:
463
464
  "sidecar": sidecar,
464
465
  "node": node,
465
466
  "probeRequested": probe_requested,
467
+ "gateway": _gateway_status(),
466
468
  **({"probe": probe} if probe is not None else {}),
467
469
  }
468
470
  if getattr(args, "json", False):
@@ -480,6 +482,51 @@ def _status(args) -> int:
480
482
  return 0 if runtime_usable and (not probe_requested or ready) else 1
481
483
 
482
484
 
485
+ def _gateway_status() -> dict:
486
+ """Project local runtime facts without changing the credential-status contract."""
487
+ unavailable = {"supported": False, "ready": False, "reason": "runtime_status_unavailable"}
488
+ try:
489
+ from gateway.status import read_runtime_status, get_runtime_status_running_pid
490
+ from hermes_constants import get_hermes_home
491
+
492
+ runtime = read_runtime_status()
493
+ # Also validate this helper contract for an absent runtime: older hosts
494
+ # lack expected_home and must not claim support until after installation.
495
+ pid = get_runtime_status_running_pid(runtime if isinstance(runtime, dict) else {}, expected_home=get_hermes_home())
496
+ if not isinstance(runtime, dict):
497
+ return {"supported": True, "ready": False, "reason": "missing_status"}
498
+ # Return only an opaque generation, never process IDs or raw state.
499
+ start_time = runtime.get("start_time")
500
+ valid_identity = type(pid) is int and pid > 0 and type(start_time) is int and start_time >= 0
501
+ if pid and not valid_identity:
502
+ return {"supported": False, "ready": False, "reason": "runtime_identity_unavailable"}
503
+ generation = hashlib.sha256(f"inline-gateway:{pid}:{start_time}".encode()).hexdigest() if valid_identity else None
504
+ state = runtime.get("gateway_state")
505
+ platforms = runtime.get("platforms")
506
+ inline = platforms.get("inline") if isinstance(platforms, dict) else None
507
+ platform_state = inline.get("state") if isinstance(inline, dict) else None
508
+ state = state if state in ("starting", "running", "draining", "stopping", "stopped", "startup_failed") else "unknown"
509
+ platform_state = platform_state if platform_state in ("connected", "connecting", "retrying", "fatal", "disconnected") else "unknown"
510
+ if pid and isinstance(inline, dict) and ("writer_pid" not in inline or "writer_start_time" not in inline):
511
+ return {"supported": False, "ready": False, "reason": "runtime_writer_identity_unavailable"}
512
+ # Runtime status can retain platform entries from the prior process.
513
+ # Only the current process's own connected projection is readiness proof.
514
+ writer_matches = isinstance(inline, dict) and inline.get("writer_pid") == pid and inline.get("writer_start_time") == start_time
515
+ ready = generation is not None and writer_matches and state == "running" and platform_state == "connected"
516
+ return {
517
+ "supported": True,
518
+ "ready": ready,
519
+ **({"generation": generation} if generation is not None else {}),
520
+ "gatewayState": state,
521
+ "platformState": platform_state,
522
+ "reason": "ready" if ready else "stale_pid" if not pid else "gateway_not_running" if state != "running" else "platform_status_stale" if not writer_matches else "platform_not_connected",
523
+ }
524
+ except Exception:
525
+ # Older Hermes versions may not expose the helpers. Never return raw
526
+ # runtime files or exceptions: they can contain provider/credential data.
527
+ return unavailable
528
+
529
+
483
530
  def _plugin_version() -> str:
484
531
  """Read the separately installed plugin version without package-manager state."""
485
532
  try:
@@ -1,7 +1,7 @@
1
1
  name: inline-platform
2
2
  label: Inline
3
3
  kind: platform
4
- version: 0.0.12
4
+ version: 0.0.14
5
5
  description: >
6
6
  Inline platform adapter for Hermes Agent. The adapter runs as a native
7
7
  Hermes Python platform plugin and supervises a local Node sidecar that uses