@inline-chat/hermes-agent-adapter 0.0.6 → 0.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/hermes-agent-adapter",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
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",
@@ -75,6 +75,7 @@
75
75
  "inlineHermes": {
76
76
  "pluginId": "inline",
77
77
  "pluginPath": "plugin/inline",
78
+ "machineSetupProtocol": 1,
78
79
  "minHermesVersion": "0.17.0",
79
80
  "testedHermesVersion": "0.19.1",
80
81
  "testedHermesCommit": "cc4cab2"
@@ -20,6 +20,8 @@ _SIDECAR_ENTRY = Path(__file__).parent / "sidecar" / "index.mjs"
20
20
  _MIN_NODE_MAJOR = 20
21
21
  _BOT_USERNAME_RE = re.compile(r"^[A-Za-z0-9_]+bot$", re.IGNORECASE)
22
22
  _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
23
+ _MAX_TOKEN_BYTES = 16 * 1024
24
+ _MACHINE_SETUP_PROTOCOL_VERSION = 1
23
25
 
24
26
 
25
27
  def gateway_setup() -> None:
@@ -205,6 +207,11 @@ def _install_inline_cli(hermes_setup) -> str | None:
205
207
 
206
208
 
207
209
  def _find_inline_cli() -> str | None:
210
+ configured = os.getenv("INLINE_CLI_BIN", "").strip()
211
+ if configured:
212
+ candidate = Path(configured).expanduser()
213
+ if candidate.is_file() and os.access(candidate, os.X_OK):
214
+ return str(candidate)
208
215
  discovered = shutil.which("inline")
209
216
  if discovered:
210
217
  return discovered
@@ -242,25 +249,56 @@ def _configure_access(hermes_gateway, hermes_setup, owner_user_id: str | None) -
242
249
  allowed.append(value)
243
250
 
244
251
  if allowed:
245
- value = ",".join(allowed)
246
- hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "false")
247
- hermes_gateway.save_env_value("INLINE_ALLOWED_USERS", value)
248
- hermes_gateway.save_env_value("INLINE_GROUP_ALLOW_FROM", value)
249
- hermes_gateway.save_env_value("INLINE_DM_POLICY", "allowlist")
250
- hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "allowlist")
252
+ _apply_access(hermes_gateway, "allowlist", owner_user_id, allowed)
251
253
  hermes_setup.print_success("Only the listed Inline users can invoke Hermes.")
252
254
  return
253
255
 
254
256
  if hermes_setup.prompt_yes_no("Allow any Inline user who can reach the bot?", False):
257
+ _apply_access(hermes_gateway, "open", owner_user_id, [])
258
+ hermes_setup.print_warning("Open access enabled. Any reachable Inline user can invoke Hermes.")
259
+ else:
260
+ _apply_access(hermes_gateway, "disabled", owner_user_id, [])
261
+ hermes_setup.print_warning("Messaging is disabled until you add allowed user IDs and re-run setup.")
262
+
263
+
264
+ def _apply_access(
265
+ hermes_gateway,
266
+ access: str,
267
+ owner_user_id: str | None,
268
+ allowed_user_ids: list[str],
269
+ ) -> list[str]:
270
+ normalized: list[str] = []
271
+ if access in ("owner", "allowlist"):
272
+ for value in [owner_user_id, *allowed_user_ids]:
273
+ candidate = str(value or "").strip()
274
+ if not candidate or not candidate.isdigit() or int(candidate) <= 0:
275
+ continue
276
+ if candidate not in normalized:
277
+ normalized.append(candidate)
278
+ if not normalized:
279
+ raise ValueError("owner or allowlist access requires a positive owner user ID")
280
+ joined = ",".join(normalized)
281
+ hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "false")
282
+ hermes_gateway.save_env_value("INLINE_ALLOWED_USERS", joined)
283
+ hermes_gateway.save_env_value("INLINE_GROUP_ALLOW_FROM", joined)
284
+ hermes_gateway.save_env_value("INLINE_DM_POLICY", "allowlist")
285
+ hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "allowlist")
286
+ return normalized
287
+ if access == "open":
255
288
  hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "true")
289
+ hermes_gateway.save_env_value("INLINE_ALLOWED_USERS", "")
290
+ hermes_gateway.save_env_value("INLINE_GROUP_ALLOW_FROM", "")
256
291
  hermes_gateway.save_env_value("INLINE_DM_POLICY", "open")
257
292
  hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "open")
258
- hermes_setup.print_warning("Open access enabled. Any reachable Inline user can invoke Hermes.")
259
- else:
293
+ return normalized
294
+ if access == "disabled":
260
295
  hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "false")
296
+ hermes_gateway.save_env_value("INLINE_ALLOWED_USERS", "")
297
+ hermes_gateway.save_env_value("INLINE_GROUP_ALLOW_FROM", "")
261
298
  hermes_gateway.save_env_value("INLINE_DM_POLICY", "disabled")
262
299
  hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "disabled")
263
- hermes_setup.print_warning("Messaging is disabled until you add allowed user IDs and re-run setup.")
300
+ return normalized
301
+ raise ValueError(f"unsupported Inline access mode: {access}")
264
302
 
265
303
 
266
304
  def _inline_cli_user_id(inline_bin: str | None) -> str | None:
@@ -297,28 +335,165 @@ def _run_inline_json(inline_bin: str, args: list[str]) -> tuple[dict | None, str
297
335
 
298
336
  def register_cli(parser: argparse.ArgumentParser) -> None:
299
337
  subs = parser.add_subparsers(dest="inline_command", required=False)
300
- subs.add_parser("setup", help="Configure Inline interactively")
301
- subs.add_parser("status", help="Show Inline adapter status")
338
+ setup = subs.add_parser("setup", help="Configure Inline")
339
+ setup.add_argument("--non-interactive", action="store_true")
340
+ setup.add_argument("--token-stdin", action="store_true")
341
+ setup.add_argument("--owner-user-id")
342
+ setup.add_argument("--access", choices=["owner", "allowlist", "open", "disabled"], default="owner")
343
+ setup.add_argument("--allow-user", action="append", default=[], type=_positive_user_id)
344
+ setup.add_argument("--json", action="store_true")
345
+ status = subs.add_parser("status", help="Show Inline adapter status")
346
+ status.add_argument("--json", action="store_true")
347
+ status.add_argument("--probe", action="store_true")
302
348
  parser.set_defaults(func=dispatch)
303
349
 
304
350
 
351
+ def _positive_user_id(value: str) -> str:
352
+ value = str(value or "").strip()
353
+ if not value.isdigit() or int(value) <= 0:
354
+ raise argparse.ArgumentTypeError("Inline user IDs must be positive integers")
355
+ return value
356
+
357
+
305
358
  def dispatch(args) -> int:
306
359
  command = getattr(args, "inline_command", None)
307
360
  if command is None:
308
361
  command = "status"
309
362
  if command == "setup":
363
+ if getattr(args, "non_interactive", False):
364
+ return _machine_setup(args)
310
365
  gateway_setup()
311
366
  return 0
312
367
  if command == "status":
313
- configured = _env_token_configured()
368
+ return _status(args)
369
+ raise SystemExit(f"unknown inline command: {command}")
370
+
371
+
372
+ def _machine_setup(args) -> int:
373
+ if not getattr(args, "token_stdin", False):
374
+ raise SystemExit("non-interactive Inline setup requires --token-stdin")
375
+ owner_user_id = str(getattr(args, "owner_user_id", "") or "").strip()
376
+ if not owner_user_id.isdigit() or int(owner_user_id) <= 0:
377
+ raise SystemExit("non-interactive Inline setup requires a positive --owner-user-id")
378
+ token = sys.stdin.read(_MAX_TOKEN_BYTES + 1)
379
+ if len(token.encode("utf-8")) > _MAX_TOKEN_BYTES:
380
+ raise SystemExit("Inline bot token exceeds the input limit")
381
+ token = token.strip()
382
+ if not token:
383
+ raise SystemExit("Inline bot token from stdin is empty")
384
+ from hermes_cli import gateway as hermes_gateway
385
+
386
+ hermes_gateway.save_env_value("INLINE_TOKEN", token)
387
+ allowed = _apply_access(
388
+ hermes_gateway,
389
+ getattr(args, "access", "owner"),
390
+ owner_user_id,
391
+ list(getattr(args, "allow_user", []) or []),
392
+ )
393
+ hermes_gateway.write_platform_config_field("inline", "enabled", True, raw=True)
394
+ result = {
395
+ "ok": True,
396
+ "action": "inline.setup",
397
+ "setupProtocolVersion": _MACHINE_SETUP_PROTOCOL_VERSION,
398
+ "pluginVersion": _plugin_version(),
399
+ "configured": True,
400
+ "access": getattr(args, "access", "owner"),
401
+ "ownerUserId": owner_user_id,
402
+ "allowedUserIds": allowed,
403
+ }
404
+ if getattr(args, "json", False):
405
+ print(json.dumps(result, separators=(",", ":")))
406
+ else:
407
+ print("Inline configured: yes")
408
+ return 0
409
+
410
+
411
+ def _status(args) -> int:
412
+ from hermes_cli import gateway as hermes_gateway
413
+
414
+ token = (
415
+ hermes_gateway.get_env_value("INLINE_TOKEN")
416
+ or hermes_gateway.get_env_value("INLINE_BOT_TOKEN")
417
+ )
418
+ configured = bool(token)
419
+ probe_requested = bool(getattr(args, "probe", False))
420
+ probe = _probe_inline_token(token) if configured and probe_requested else None
421
+ ready = configured and (not probe_requested or bool(probe and probe.get("ok")))
422
+ result = {
423
+ "ok": ready,
424
+ "action": "inline.status",
425
+ "setupProtocolVersion": _MACHINE_SETUP_PROTOCOL_VERSION,
426
+ "pluginVersion": _plugin_version(),
427
+ "configured": configured,
428
+ "sidecarBundled": _SIDECAR_ENTRY.exists(),
429
+ "node": _node_status(),
430
+ "probeRequested": probe_requested,
431
+ **({"probe": probe} if probe is not None else {}),
432
+ }
433
+ if getattr(args, "json", False):
434
+ print(json.dumps(result, separators=(",", ":")))
435
+ else:
314
436
  print(f"Inline configured: {'yes' if configured else 'no'}")
315
437
  print(f"Inline sidecar bundled: {'yes' if _SIDECAR_ENTRY.exists() else 'no'}")
316
438
  print(f"Node available: {_node_status()}")
317
439
  if not configured:
318
440
  print("Next: run `hermes inline setup` for guided bot setup.")
441
+ elif probe_requested:
442
+ print(f"Inline credential probe: {'ready' if ready else 'failed'}")
319
443
  print("Advanced diagnostics: inline-hermes doctor --json")
320
- return 0
321
- raise SystemExit(f"unknown inline command: {command}")
444
+ return 0 if not probe_requested or ready else 1
445
+
446
+
447
+ def _plugin_version() -> str:
448
+ """Read the separately installed plugin version without package-manager state."""
449
+ try:
450
+ for line in (Path(__file__).parent / "plugin.yaml").read_text(encoding="utf-8").splitlines():
451
+ key, separator, value = line.partition(":")
452
+ if separator and key.strip() == "version":
453
+ version = value.strip().strip("\"'")
454
+ if version:
455
+ return version
456
+ except OSError:
457
+ pass
458
+ return "unknown"
459
+
460
+
461
+ def _probe_inline_token(token: str) -> dict:
462
+ inline_bin = _find_inline_cli()
463
+ if not inline_bin:
464
+ return {"ok": False, "error": "Inline CLI was not found for the credential probe."}
465
+ env = os.environ.copy()
466
+ for name in ("INLINE_TOKEN", "INLINE_BOT_TOKEN", "INLINE_OWNER_TOKEN", "INLINE_ACCESS_TOKEN"):
467
+ env.pop(name, None)
468
+ env["INLINE_TOKEN"] = token
469
+ try:
470
+ result = subprocess.run(
471
+ [inline_bin, "--json", "--compact", "auth", "me"],
472
+ stdout=subprocess.PIPE,
473
+ stderr=subprocess.PIPE,
474
+ text=True,
475
+ timeout=30,
476
+ check=False,
477
+ env=env,
478
+ )
479
+ except (OSError, subprocess.TimeoutExpired):
480
+ return {"ok": False, "error": "Inline credential probe could not run."}
481
+ if result.returncode != 0:
482
+ return {"ok": False, "error": "Inline rejected the configured credential."}
483
+ try:
484
+ payload = json.loads(result.stdout)
485
+ except (json.JSONDecodeError, TypeError):
486
+ return {"ok": False, "error": "Inline credential probe returned unreadable output."}
487
+ raw_id = payload.get("id") if isinstance(payload, dict) else None
488
+ bot_user_id = str(raw_id).strip() if raw_id is not None else ""
489
+ if not bot_user_id.isdigit() or int(bot_user_id) <= 0:
490
+ return {"ok": False, "error": "Inline credential probe returned no bot identity."}
491
+ username = str(payload.get("username") or "").strip().lstrip("@")
492
+ return {
493
+ "ok": True,
494
+ "botUserId": bot_user_id,
495
+ **({"botUsername": username} if username else {}),
496
+ }
322
497
 
323
498
 
324
499
  def _env_token_configured() -> bool:
@@ -1,7 +1,7 @@
1
1
  name: inline-platform
2
2
  label: Inline
3
3
  kind: platform
4
- version: 0.0.6
4
+ version: 0.0.7
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