@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/README.md +18 -1
- package/dist/install.js +72 -72
- package/package.json +2 -1
- package/plugin/inline/cli.py +189 -14
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +71 -68
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.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"
|
package/plugin/inline/cli.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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:
|