@inline-chat/hermes-agent-adapter 0.0.18-alpha.0 → 0.0.19

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.
@@ -22,7 +22,6 @@ import signal
22
22
  import socket
23
23
  import subprocess
24
24
  import sys
25
- import threading
26
25
  import time
27
26
  from collections import OrderedDict
28
27
  from datetime import datetime, timezone
@@ -45,6 +44,7 @@ from gateway.platforms.base import (
45
44
  SendResult,
46
45
  cache_audio_from_url,
47
46
  cache_image_from_url,
47
+ safe_url_for_log,
48
48
  )
49
49
  from gateway.platforms.helpers import strip_markdown
50
50
 
@@ -106,15 +106,8 @@ _INLINE_THREADS_COMMAND_DESCRIPTION = "Configure Inline reply-thread routing"
106
106
  _INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto|reset]"
107
107
  _INLINE_FOLLOW_COMMAND_DESCRIPTION = "Explicitly follow this Inline chat or thread"
108
108
  _INLINE_UNFOLLOW_COMMAND_DESCRIPTION = "Explicitly unfollow this Inline chat or thread"
109
- _INLINE_UPDATE_COMMAND_DESCRIPTION = "Update the Inline Hermes plugin"
110
109
  _INLINE_SYNC_COMMAND_DESCRIPTION = "Resync Inline commands and skills"
111
110
  _INLINE_VERSION_COMMAND_DESCRIPTION = "Show Inline plugin and sync information"
112
- _INLINE_UPDATE_PACKAGE_NAME = "@inline-chat/hermes-agent-adapter"
113
- _INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS = 30
114
- _INLINE_UPDATE_TIMEOUT_SECONDS = 5 * 60
115
- _INLINE_UPDATE_LOG_MAX_LINES = 80
116
- _INLINE_UPDATE_LOG_MAX_CHARS = 8_000
117
- _INLINE_UPDATE_LOCK = threading.Lock()
118
111
  _INLINE_THREADS_ACTION_PREFIX = "th:"
119
112
  _INLINE_THREADS_ACTION_TTL_SECONDS = 15 * 60
120
113
  _INLINE_THREAD_COMMAND_RE = re.compile(r"^/(?:thread|threads)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
@@ -575,7 +568,7 @@ def _normalize_inline_command_description(raw: str) -> str:
575
568
 
576
569
 
577
570
  def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[List[Dict[str, Any]], int]:
578
- from hermes_cli.commands import telegram_menu_commands
571
+ from hermes_cli.commands_platforms import telegram_menu_commands
579
572
 
580
573
  command_specs = _inline_command_specs()
581
574
  local_commands = [
@@ -1209,7 +1202,7 @@ class InlineAdapter(BasePlatformAdapter):
1209
1202
 
1210
1203
  try:
1211
1204
  from gateway.run import _load_gateway_config
1212
- from hermes_cli.model_switch import list_picker_providers
1205
+ from hermes_cli.model_switch_providers import list_picker_providers
1213
1206
 
1214
1207
  cfg = _load_gateway_config() or {}
1215
1208
  model_cfg = cfg.get("model") if isinstance(cfg, dict) else {}
@@ -3001,16 +2994,15 @@ class InlineAdapter(BasePlatformAdapter):
3001
2994
  return await cache_audio_from_url(url, ext=_extension_for_media(mime, file_name, ".ogg"))
3002
2995
  return await self._download_inline_media_url(url, mime=mime, file_name=file_name)
3003
2996
  except Exception as exc:
3004
- logger.warning("[inline] failed to cache %s attachment: %s", kind, exc)
3005
- return url
2997
+ logger.warning("[inline] failed to cache %s attachment (%s)", kind, type(exc).__name__)
2998
+ # Never pass an unvalidated URL on to Hermes for another fetch.
2999
+ return None
3006
3000
 
3007
3001
  async def _download_inline_media_url(self, url: str, *, mime: str, file_name: Optional[str]) -> str:
3008
- try:
3009
- from tools.url_safety import is_safe_url, safe_url_for_log
3010
- if not is_safe_url(url):
3011
- raise ValueError(f"blocked unsafe media URL: {safe_url_for_log(url)}")
3012
- except ImportError:
3013
- pass
3002
+ # A missing host safety helper must stop the download, never bypass it.
3003
+ from tools.url_safety import is_safe_url
3004
+ if not is_safe_url(url):
3005
+ raise ValueError(f"blocked unsafe media URL: {safe_url_for_log(url)}")
3014
3006
  _MEDIA_CACHE_DIR.mkdir(parents=True, exist_ok=True)
3015
3007
  name = _safe_media_file_name(url=url, mime=mime, file_name=file_name)
3016
3008
  path = _MEDIA_CACHE_DIR / name
@@ -4230,7 +4222,7 @@ class InlineAdapter(BasePlatformAdapter):
4230
4222
  label = group_id
4231
4223
  member_slugs: list[str] = []
4232
4224
  try:
4233
- from hermes_cli.models import PROVIDER_GROUPS
4225
+ from hermes_cli.models_catalog_static import PROVIDER_GROUPS
4234
4226
  label, _desc, members = PROVIDER_GROUPS.get(group_id, (group_id, "", []))
4235
4227
  member_slugs = [str(member) for member in members]
4236
4228
  except Exception:
@@ -4686,7 +4678,6 @@ class InlineAdapter(BasePlatformAdapter):
4686
4678
 
4687
4679
  async def send_image(self, chat_id: str, image_url: str, caption: Optional[str] = None, reply_to: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> SendResult:
4688
4680
  try:
4689
- from gateway.platforms.base import cache_image_from_url
4690
4681
  local_path = await cache_image_from_url(image_url)
4691
4682
  return await self.send_image_file(chat_id, local_path, caption, reply_to, metadata)
4692
4683
  except Exception:
@@ -4694,7 +4685,6 @@ class InlineAdapter(BasePlatformAdapter):
4694
4685
 
4695
4686
  async def send_animation(self, chat_id: str, animation_url: str, caption: Optional[str] = None, reply_to: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> SendResult:
4696
4687
  try:
4697
- from gateway.platforms.base import cache_image_from_url
4698
4688
  local_path = await cache_image_from_url(animation_url, ext=".gif")
4699
4689
  return await self.send_document(chat_id, local_path, caption, file_name=Path(local_path).name, reply_to=reply_to, metadata=metadata)
4700
4690
  except Exception:
@@ -4982,7 +4972,7 @@ class InlineAdapter(BasePlatformAdapter):
4982
4972
  }
4983
4973
  actions: list[Dict[str, str]] = []
4984
4974
  try:
4985
- from hermes_cli.models import group_providers
4975
+ from hermes_cli.models_catalog_static import group_providers
4986
4976
  grouped = group_providers(list(by_slug.keys()))
4987
4977
  except Exception:
4988
4978
  grouped = [{"kind": "single", "slug": slug} for slug in by_slug.keys()]
@@ -5319,67 +5309,8 @@ def _inline_unfollow_command_handler(raw_args: str = "") -> str:
5319
5309
  return _inline_follow_command_fallback("unfollow", raw_args)
5320
5310
 
5321
5311
 
5322
- def _inline_update_environment() -> Dict[str, str]:
5323
- try:
5324
- from tools.environments.local import _sanitize_subprocess_env
5325
- env = _sanitize_subprocess_env(os.environ.copy())
5326
- except Exception:
5327
- env = os.environ.copy()
5328
- # Hermes cannot know the secret names introduced by every external plugin.
5329
- sensitive_names = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTHORIZATION")
5330
- for key in list(env):
5331
- if any(name in key.upper() for name in sensitive_names):
5332
- env.pop(key, None)
5333
- return env
5334
-
5335
-
5336
- def _inline_update_log_text(raw: Any, hermes_home: Optional[Path] = None) -> str:
5337
- text = str(raw or "")
5338
- text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
5339
- text = re.sub(r"(?i)\b(Bearer\s+)[^\s]+", r"\1[REDACTED]", text)
5340
- text = re.sub(r"(?i)(https?://)[^/\s:@]+:[^@\s/]+@", r"\1[REDACTED]@", text)
5341
- text = re.sub(r"([?&][^=\s&]+)=([^&\s]+)", r"\1=[REDACTED]", text)
5342
- text = re.sub(
5343
- r"(?i)\b([A-Za-z0-9_-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_-]*)\s*([=:])\s*[^\s]+",
5344
- r"\1\2[REDACTED]",
5345
- text,
5346
- )
5347
- sensitive_names = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTHORIZATION")
5348
- for key, value in os.environ.items():
5349
- if len(value) >= 8 and any(name in key.upper() for name in sensitive_names):
5350
- text = text.replace(value, "[REDACTED]")
5351
- private_paths = []
5352
- if hermes_home:
5353
- private_paths.append((str(hermes_home), "$HERMES_HOME"))
5354
- private_paths.append((str(Path.home()), "~"))
5355
- for path_text, replacement in private_paths:
5356
- if len(path_text) > 1:
5357
- text = text.replace(path_text, replacement)
5358
- text = "".join(char if char in "\n\t" or ord(char) >= 32 else "?" for char in text)
5359
- lines = [line.rstrip() for line in text.splitlines() if line.strip()]
5360
- text = "\n".join(lines[-_INLINE_UPDATE_LOG_MAX_LINES:])
5361
- if len(text) > _INLINE_UPDATE_LOG_MAX_CHARS:
5362
- text = "[earlier output truncated]\n" + text[-_INLINE_UPDATE_LOG_MAX_CHARS:]
5363
- return text or "(no subprocess output)"
5364
-
5365
-
5366
- def _log_inline_update_failure(
5367
- stage: str,
5368
- detail: str,
5369
- *,
5370
- output: Any = None,
5371
- hermes_home: Optional[Path] = None,
5372
- ) -> None:
5373
- diagnostic = f"{detail}\n{output or ''}".strip()
5374
- logger.error(
5375
- "[inline-update] stage=%s failed\n%s",
5376
- stage,
5377
- _inline_update_log_text(diagnostic, hermes_home),
5378
- )
5379
-
5380
-
5381
- def _installed_inline_plugin_version(hermes_home: Path) -> Optional[str]:
5382
- manifest = hermes_home / "plugins" / "inline" / "plugin.yaml"
5312
+ def _installed_inline_plugin_version() -> Optional[str]:
5313
+ manifest = Path(__file__).resolve().with_name("plugin.yaml")
5383
5314
  try:
5384
5315
  text = manifest.read_text(encoding="utf-8")
5385
5316
  except OSError:
@@ -5388,8 +5319,8 @@ def _installed_inline_plugin_version(hermes_home: Path) -> Optional[str]:
5388
5319
  return match.group(1) if match else None
5389
5320
 
5390
5321
 
5391
- def _inline_install_timestamp(hermes_home: Path) -> Optional[str]:
5392
- target = hermes_home / "plugins" / "inline"
5322
+ def _inline_install_timestamp() -> Optional[str]:
5323
+ target = Path(__file__).resolve().parent
5393
5324
  try:
5394
5325
  timestamp = target.lstat().st_ctime
5395
5326
  except OSError:
@@ -5406,21 +5337,13 @@ def _inline_catalog_part_summary(part: Dict[str, Any]) -> str:
5406
5337
 
5407
5338
 
5408
5339
  def _inline_version_text(last_sync: Optional[Dict[str, Any]] = None) -> str:
5409
- hermes_home = Path(os.getenv("HERMES_HOME") or Path.home() / ".hermes").expanduser()
5410
- version = _installed_inline_plugin_version(hermes_home)
5411
- if not version:
5412
- try:
5413
- manifest = Path(__file__).with_name("plugin.yaml").read_text(encoding="utf-8")
5414
- except OSError:
5415
- manifest = ""
5416
- match = re.search(r"(?m)^version:\s*['\"]?([^\s'\"]+)", manifest)
5417
- version = match.group(1) if match else None
5340
+ version = _installed_inline_plugin_version()
5418
5341
  try:
5419
5342
  from hermes_cli import __version__ as hermes_version
5420
5343
  except Exception:
5421
5344
  hermes_version = None
5422
5345
 
5423
- installed_at = _inline_install_timestamp(hermes_home)
5346
+ installed_at = _inline_install_timestamp()
5424
5347
  if last_sync:
5425
5348
  sync_text = (
5426
5349
  f"{last_sync.get('completed_at') or 'unknown'} "
@@ -5439,178 +5362,6 @@ def _inline_version_text(last_sync: Optional[Dict[str, Any]] = None) -> str:
5439
5362
  ])
5440
5363
 
5441
5364
 
5442
- def _inline_update_lane(version: Optional[str]) -> Optional[str]:
5443
- if not version:
5444
- return None
5445
- match = re.fullmatch(r"\d+\.\d+\.\d+(?:-([A-Za-z][A-Za-z0-9-]*)(?:\.[0-9A-Za-z-]+)*)?", version)
5446
- if not match:
5447
- return None
5448
- return match.group(1) or "latest"
5449
-
5450
-
5451
- def _semver_core(version: Any) -> Optional[tuple[int, int, int]]:
5452
- match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$", str(version or "").strip())
5453
- if not match:
5454
- return None
5455
- return tuple(int(part) for part in match.groups())
5456
-
5457
-
5458
- def _run_inline_update() -> str:
5459
- if not _INLINE_UPDATE_LOCK.acquire(blocking=False):
5460
- return "An Inline plugin update is already running."
5461
- try:
5462
- return _run_inline_update_locked()
5463
- finally:
5464
- _INLINE_UPDATE_LOCK.release()
5465
-
5466
-
5467
- def _run_inline_update_locked() -> str:
5468
- hermes_home = Path(os.getenv("HERMES_HOME") or Path.home() / ".hermes").expanduser()
5469
- npm_bin = shutil.which("npm")
5470
- if not npm_bin:
5471
- _log_inline_update_failure("precheck", "npm was not found on PATH", hermes_home=hermes_home)
5472
- return "Inline plugin update is unavailable because npm was not found on PATH. Details were written to Hermes logs under `[inline-update]`."
5473
-
5474
- target = hermes_home / "plugins" / "inline"
5475
- if target.is_symlink():
5476
- return (
5477
- "Inline is installed as a development symlink, so automatic update was skipped. "
5478
- "Update the linked source checkout and restart Hermes instead."
5479
- )
5480
-
5481
- installed_version = _installed_inline_plugin_version(hermes_home)
5482
- lane = _inline_update_lane(installed_version)
5483
- if not lane:
5484
- return (
5485
- "Inline plugin update could not determine the installed release channel. "
5486
- "Update it manually with the package version or npm dist-tag you want to follow."
5487
- )
5488
- package_spec = f"{_INLINE_UPDATE_PACKAGE_NAME}@{lane}"
5489
- precheck_command = [npm_bin, "view", package_spec, "--json"]
5490
- try:
5491
- precheck = subprocess.run(
5492
- precheck_command,
5493
- stdout=subprocess.PIPE,
5494
- stderr=subprocess.PIPE,
5495
- text=True,
5496
- timeout=_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS,
5497
- check=False,
5498
- env=_inline_update_environment(),
5499
- )
5500
- except subprocess.TimeoutExpired as exc:
5501
- _log_inline_update_failure(
5502
- "precheck",
5503
- "npm view timed out",
5504
- output=f"{exc.stdout or ''}\n{exc.stderr or ''}",
5505
- hermes_home=hermes_home,
5506
- )
5507
- return "Inline plugin compatibility precheck timed out; no update was applied. Details were written to Hermes logs under `[inline-update]`."
5508
- except OSError as exc:
5509
- _log_inline_update_failure("precheck", f"npm view could not start: {exc}", hermes_home=hermes_home)
5510
- return "Inline plugin compatibility precheck could not start; no update was applied. Details were written to Hermes logs under `[inline-update]`."
5511
- if precheck.returncode != 0:
5512
- _log_inline_update_failure(
5513
- "precheck",
5514
- f"npm view exited with code {precheck.returncode} for {package_spec}",
5515
- output=f"{precheck.stdout or ''}\n{precheck.stderr or ''}",
5516
- hermes_home=hermes_home,
5517
- )
5518
- return (
5519
- f"Inline plugin compatibility precheck failed with exit code {precheck.returncode}; "
5520
- "no update was applied. Details were written to Hermes logs under `[inline-update]`."
5521
- )
5522
- try:
5523
- package_metadata = json.loads(precheck.stdout or "{}")
5524
- except (TypeError, json.JSONDecodeError):
5525
- package_metadata = None
5526
- inline_metadata = package_metadata.get("inlineHermes") if isinstance(package_metadata, dict) else None
5527
- candidate_version = package_metadata.get("version") if isinstance(package_metadata, dict) else None
5528
- minimum_hermes = inline_metadata.get("minHermesVersion") if isinstance(inline_metadata, dict) else None
5529
- try:
5530
- from hermes_cli import __version__ as current_hermes
5531
- except Exception:
5532
- current_hermes = None
5533
- current_core = _semver_core(current_hermes)
5534
- minimum_core = _semver_core(minimum_hermes)
5535
- if not candidate_version or not current_core or not minimum_core:
5536
- _log_inline_update_failure(
5537
- "precheck",
5538
- f"invalid compatibility metadata for {package_spec}; current Hermes version={current_hermes!r}",
5539
- output=precheck.stdout,
5540
- hermes_home=hermes_home,
5541
- )
5542
- return (
5543
- "Inline plugin compatibility metadata could not be verified; "
5544
- "no update was applied. Details were written to Hermes logs under `[inline-update]`."
5545
- )
5546
- if current_core < minimum_core:
5547
- return (
5548
- f"Inline plugin `{candidate_version}` requires Hermes `{minimum_hermes}` or newer, "
5549
- f"but this agent is running `{current_hermes}`. Update Hermes first; no plugin update was applied."
5550
- )
5551
-
5552
- command = [
5553
- npm_bin,
5554
- "exec",
5555
- "--yes",
5556
- f"--package={package_spec}",
5557
- "--",
5558
- "inline-hermes",
5559
- "install",
5560
- "--force",
5561
- "--hermes-home",
5562
- str(hermes_home),
5563
- ]
5564
- try:
5565
- result = subprocess.run(
5566
- command,
5567
- stdout=subprocess.PIPE,
5568
- stderr=subprocess.STDOUT,
5569
- text=True,
5570
- timeout=_INLINE_UPDATE_TIMEOUT_SECONDS,
5571
- check=False,
5572
- env=_inline_update_environment(),
5573
- )
5574
- except subprocess.TimeoutExpired as exc:
5575
- _log_inline_update_failure(
5576
- "install",
5577
- f"npm exec timed out for {package_spec}",
5578
- output=exc.stdout,
5579
- hermes_home=hermes_home,
5580
- )
5581
- return (
5582
- "Inline plugin update timed out. Run "
5583
- f"`npm exec --yes --package={package_spec} -- "
5584
- "inline-hermes install --force` on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
5585
- )
5586
- except OSError as exc:
5587
- _log_inline_update_failure("install", f"npm exec could not start: {exc}", hermes_home=hermes_home)
5588
- return "Inline plugin update could not start. Check that npm is installed and usable on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
5589
-
5590
- if result.returncode != 0:
5591
- _log_inline_update_failure(
5592
- "install",
5593
- f"npm exec exited with code {result.returncode} for {package_spec}",
5594
- output=result.stdout,
5595
- hermes_home=hermes_home,
5596
- )
5597
- return (
5598
- f"Inline plugin update failed with exit code {result.returncode}. Run "
5599
- f"`npm exec --yes --package={package_spec} -- "
5600
- "inline-hermes install --force` on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
5601
- )
5602
-
5603
- version = _installed_inline_plugin_version(hermes_home)
5604
- version_text = f" to `{version}`" if version else ""
5605
- return f"Inline plugin updated{version_text} from the `{lane}` channel. Run `/restart` to load the new version."
5606
-
5607
-
5608
- async def _inline_update_command_handler(raw_args: str = "") -> str:
5609
- if str(raw_args or "").strip():
5610
- return "Usage: `/inline_update`"
5611
- return await asyncio.to_thread(_run_inline_update)
5612
-
5613
-
5614
5365
  async def _inline_sync_command_handler(raw_args: str = "") -> str:
5615
5366
  if str(raw_args or "").strip():
5616
5367
  return "Usage: `/inline_sync`"
@@ -5641,11 +5392,6 @@ def _inline_command_specs() -> tuple[_InlineCommandSpec, ...]:
5641
5392
  handler=_inline_unfollow_command_handler,
5642
5393
  description=_INLINE_UNFOLLOW_COMMAND_DESCRIPTION,
5643
5394
  ),
5644
- _InlineCommandSpec(
5645
- name="inline-update",
5646
- handler=_inline_update_command_handler,
5647
- description=_INLINE_UPDATE_COMMAND_DESCRIPTION,
5648
- ),
5649
5395
  _InlineCommandSpec(
5650
5396
  name="inline-sync",
5651
5397
  handler=_inline_sync_command_handler,
@@ -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.18-alpha.0"
30
+ _PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.19"
31
31
  _ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
32
32
 
33
33
 
@@ -362,6 +362,7 @@ def register_cli(parser: argparse.ArgumentParser) -> None:
362
362
  setup.add_argument("--json", action="store_true", help="Print compact machine-readable setup output.")
363
363
  status = subs.add_parser("status", help="Show Inline adapter status", description="Check Inline configuration, sidecar, Node runtime, and optional credential identity.")
364
364
  status.add_argument("--json", action="store_true", help="Print compact machine-readable status output.")
365
+ status.add_argument("--check-compatibility", action="store_true", help="Validate the installed plugin with the Hermes loader.")
365
366
  status.add_argument("--probe", action="store_true", help="Verify the configured Inline credential and bot identity.")
366
367
  parser.set_defaults(func=dispatch)
367
368
 
@@ -453,7 +454,9 @@ def _status(args) -> int:
453
454
  and sidecar["size"] > 0
454
455
  )
455
456
  runtime_usable = bool(sidecar["ok"] and node["ok"])
456
- ready = runtime_usable and configured and (not probe_requested or bool(probe and probe.get("ok")))
457
+ compatibility = _compatibility_status() if getattr(args, "check_compatibility", False) else None
458
+ compatible = compatibility is None or compatibility["ok"]
459
+ ready = compatible and runtime_usable and configured and (not probe_requested or bool(probe and probe.get("ok")))
457
460
  result = {
458
461
  "ok": ready,
459
462
  "ready": ready,
@@ -467,11 +470,14 @@ def _status(args) -> int:
467
470
  "node": node,
468
471
  "probeRequested": probe_requested,
469
472
  "gateway": _gateway_status(),
473
+ **({"compatibility": compatibility} if compatibility is not None else {}),
470
474
  **({"probe": probe} if probe is not None else {}),
471
475
  }
472
476
  if getattr(args, "json", False):
473
477
  print(json.dumps(result, separators=(",", ":")))
474
478
  else:
479
+ if compatibility is not None:
480
+ print(f"Hermes plugin compatible: {'yes' if compatibility['ok'] else 'no'} ({compatibility['reason']})")
475
481
  print(f"Inline configured: {'yes' if configured else 'no'}")
476
482
  print(f"Inline sidecar usable: {'yes' if sidecar['ok'] else 'no'}")
477
483
  print(f"Node available: {_node_status_text(node)}")
@@ -481,7 +487,46 @@ def _status(args) -> int:
481
487
  elif probe_requested:
482
488
  print(f"Inline credential probe: {'ready' if ready else 'failed'}")
483
489
  print("Advanced diagnostics: inline-hermes doctor --json")
484
- return 0 if runtime_usable and (not probe_requested or ready) else 1
490
+ return 0 if compatible and runtime_usable and (not probe_requested or ready) else 1
491
+
492
+
493
+ def _compatibility_status() -> dict:
494
+ """Attest only a successful host load of this exact installed plugin.
495
+
496
+ Never return host exception text: configuration or import errors may contain
497
+ credentials. A missing diagnostic API is an inconclusive check, not success.
498
+ """
499
+ unavailable = {"ok": False, "reason": "loader_unavailable"}
500
+ try:
501
+ from hermes_cli.plugins import get_plugin_manager
502
+
503
+ manager = get_plugin_manager()
504
+ manager.discover_and_load()
505
+ plugin_dir = Path(__file__).resolve().parent
506
+ loaded = next((
507
+ item for item in manager._plugins.values()
508
+ if item.manifest.name == "inline-platform"
509
+ and Path(item.manifest.path).resolve() == plugin_dir
510
+ ), None)
511
+ if loaded is None:
512
+ return {"ok": False, "reason": "plugin_not_loaded"}
513
+ if not loaded.enabled or loaded.error or loaded.module is None:
514
+ return {"ok": False, "reason": "plugin_load_failed"}
515
+ if "inline" not in loaded.tools_registered:
516
+ return {"ok": False, "reason": "tool_not_registered"}
517
+ # Older supported Hermes releases predate the moved-import scanner.
518
+ # Their loader still checks real imports and plugin registration above.
519
+ try:
520
+ from hermes_cli.plugin_compat import plugin_hits
521
+ except ModuleNotFoundError as error:
522
+ if error.name != "hermes_cli.plugin_compat":
523
+ raise
524
+ else:
525
+ if plugin_hits(loaded.manifest):
526
+ return {"ok": False, "reason": "deprecated_imports"}
527
+ return {"ok": True, "reason": "loaded", "pluginPath": str(plugin_dir)}
528
+ except Exception:
529
+ return unavailable
485
530
 
486
531
 
487
532
  def _gateway_status() -> dict:
@@ -1,7 +1,10 @@
1
1
  name: inline-platform
2
2
  label: Inline
3
3
  kind: platform
4
- version: 0.0.18-alpha.0
4
+ provides_tools:
5
+ - inline
6
+ version: 0.0.19
7
+ requires_hermes: ">=0.21.4"
5
8
  description: >
6
9
  Inline platform adapter for Hermes Agent. The adapter runs as a native
7
10
  Hermes Python platform plugin and supervises a local Node sidecar that uses