@misterhuydo/sentinel 1.0.53 → 1.0.55

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/.cairn/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-03-22T11:21:17.183Z
1
+ 2026-03-22T12:03:28.852Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T11:32:35.191Z",
3
- "checkpoint_at": "2026-03-22T11:32:35.192Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T12:06:59.426Z",
3
+ "checkpoint_at": "2026-03-22T12:06:59.427Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -61,6 +61,7 @@ class SentinelConfig:
61
61
  slack_app_token: str = "" # xapp-... (Socket Mode)
62
62
  slack_channel: str = "" # optional: restrict to one channel ID or name
63
63
  slack_watch_bot_ids: list[str] = field(default_factory=list) # pre-configured bot IDs to watch passively
64
+ slack_allowed_users: list[str] = field(default_factory=list) # if set, only these Slack user IDs can talk to Boss
64
65
  project_name: str = "" # optional: friendly name used by Sentinel Boss (e.g. "1881")
65
66
 
66
67
 
@@ -155,6 +156,7 @@ class ConfigLoader:
155
156
  c.slack_app_token = d.get("SLACK_APP_TOKEN", "")
156
157
  c.slack_channel = d.get("SLACK_CHANNEL", "")
157
158
  c.slack_watch_bot_ids = _csv(d.get("SLACK_WATCH_BOT_IDS", ""))
159
+ c.slack_allowed_users = _csv(d.get("SLACK_ALLOWED_USERS", ""))
158
160
  c.project_name = d.get("PROJECT_NAME", "")
159
161
  self.sentinel = c
160
162
 
@@ -95,6 +95,10 @@ What you can do (tools available):
95
95
  they are delivering to.
96
96
  e.g. "which bots are you watching?", "list monitored bots"
97
97
 
98
+ 18. upgrade_sentinel — Pull the latest Sentinel agent code, update Python deps, and restart the
99
+ process. Safe to run at any time — no restart if already up to date.
100
+ e.g. "upgrade sentinel", "update sentinel", "upgrade yourself"
101
+
98
102
  When someone asks what you can do, what you support, what your capabilities are, or how you can help,
99
103
  reply with a short summary grouped by category:
100
104
 
@@ -126,6 +130,9 @@ reply with a short summary grouped by category:
126
130
  • `unwatch_bot` — stop monitoring a bot — "stop watching @errorbot"
127
131
  • `list_watched_bots` — show all bots currently being monitored — "which bots are you watching?"
128
132
 
133
+ *Self-management*
134
+ • `upgrade_sentinel` — git pull + pip install + restart — "upgrade sentinel", "update yourself"
135
+
129
136
  Tone: direct, professional, like a senior engineer who owns the system.
130
137
  Don't pad responses. Don't say "Great question!" or "Certainly!".
131
138
  If you don't know something, use a tool to find out before saying you don't know.
@@ -409,6 +416,17 @@ _TOOLS = [
409
416
  ),
410
417
  "input_schema": {"type": "object", "properties": {}},
411
418
  },
419
+ {
420
+ "name": "upgrade_sentinel",
421
+ "description": (
422
+ "Upgrade the Sentinel agent itself: git pull the latest code, update Python deps, "
423
+ "then restart the process. Safe to call at any time — if already up to date, "
424
+ "no restart is triggered. "
425
+ "Use for: 'upgrade sentinel', 'update sentinel', 'upgrade yourself', "
426
+ "'pull latest sentinel code', 'restart sentinel after upgrade'."
427
+ ),
428
+ "input_schema": {"type": "object", "properties": {}},
429
+ },
412
430
  ]
413
431
 
414
432
 
@@ -854,6 +872,73 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
854
872
  ],
855
873
  })
856
874
 
875
+ if name == "upgrade_sentinel":
876
+ import threading
877
+ import signal as _sig
878
+
879
+ code_dir = Path(__file__).resolve().parent.parent # sentinel repo root
880
+ project_dir = Path(".").resolve()
881
+ steps: list[dict] = []
882
+
883
+ # Step 1: git pull the sentinel agent code
884
+ pull = _git_pull(code_dir)
885
+ steps.append({"step": "git_pull", **pull})
886
+ already_latest = "already up to date" in pull.get("detail", "").lower()
887
+
888
+ if pull["status"] == "error":
889
+ return json.dumps({"status": "error", "steps": steps,
890
+ "note": "git pull failed — check network / SSH key"})
891
+
892
+ # Step 2: pip install (update Python deps)
893
+ venv_pip = code_dir / ".venv" / "bin" / "pip"
894
+ pip_cmd = str(venv_pip) if venv_pip.exists() else "pip3"
895
+ req_file = code_dir / "requirements.txt"
896
+ if req_file.exists():
897
+ try:
898
+ r = subprocess.run(
899
+ [pip_cmd, "install", "-q", "-r", str(req_file)],
900
+ capture_output=True, text=True, timeout=120,
901
+ )
902
+ steps.append({
903
+ "step": "pip_install",
904
+ "status": "ok" if r.returncode == 0 else "warn",
905
+ "detail": (r.stderr or "").strip()[:200],
906
+ })
907
+ except Exception as e:
908
+ steps.append({"step": "pip_install", "status": "warn", "detail": str(e)})
909
+
910
+ if already_latest:
911
+ return json.dumps({
912
+ "status": "already_latest",
913
+ "steps": steps,
914
+ "note": "Sentinel is already up to date. No restart needed.",
915
+ })
916
+
917
+ # Step 3: restart — schedule after response is sent
918
+ stop_sh = project_dir / "stop.sh"
919
+ start_sh = project_dir / "start.sh"
920
+ if stop_sh.exists() and start_sh.exists():
921
+ def _restart_scripts():
922
+ import time; time.sleep(2)
923
+ subprocess.Popen(
924
+ f"bash {stop_sh} && sleep 2 && bash {start_sh}",
925
+ shell=True,
926
+ )
927
+ threading.Thread(target=_restart_scripts, daemon=True).start()
928
+ restart_method = "stop.sh + start.sh"
929
+ else:
930
+ # SIGTERM self — systemd (Restart=always) will bring it back up
931
+ threading.Timer(2.0, lambda: os.kill(os.getpid(), _sig.SIGTERM)).start()
932
+ restart_method = "SIGTERM → systemd restart"
933
+
934
+ steps.append({"step": "restart", "status": "scheduled", "method": restart_method})
935
+ logger.info("Boss: upgrade_sentinel complete, restarting via %s", restart_method)
936
+ return json.dumps({
937
+ "status": "ok",
938
+ "steps": steps,
939
+ "note": "Upgrade complete. Sentinel is restarting — give it a few seconds then I'll be back.",
940
+ })
941
+
857
942
  return json.dumps({"error": f"unknown tool: {name}"})
858
943
 
859
944
 
@@ -293,6 +293,12 @@ async def _dispatch(event: dict, client, cfg_loader, store) -> None:
293
293
  if not text:
294
294
  return
295
295
 
296
+ # Allowlist check — if SLACK_ALLOWED_USERS is configured, silently ignore everyone else
297
+ allowed = cfg_loader.sentinel.slack_allowed_users
298
+ if allowed and user_id not in allowed:
299
+ logger.warning("Boss: ignoring message from unauthorised user %s", user_id)
300
+ return
301
+
296
302
  user_name = await _resolve_name(client, user_id)
297
303
 
298
304
  status, pos, session = await _queue.try_activate(user_id, user_name, channel)
@@ -36,6 +36,12 @@ WORKSPACE_DIR=./workspace
36
36
  # Note: requires conversations:read scope on the Slack App if using channel name
37
37
  # SLACK_CHANNEL=devops-sentinel
38
38
 
39
+ # Allowlist of Slack user IDs permitted to give Sentinel Boss commands (RECOMMENDED).
40
+ # If set, all other users are silently ignored — even in the configured channel.
41
+ # Find a user ID in Slack: click their profile → ⋯ More → Copy member ID (starts with U).
42
+ # Comma-separated. Leave unset to allow anyone who can reach the bot (less secure).
43
+ # SLACK_ALLOWED_USERS=U01AB2CD3EF, U09GH8IJ7KL
44
+
39
45
  # Passive bot watcher — seed the watch list on startup with known bot IDs.
40
46
  # Sentinel will passively queue every message from these bots as issues (no @mention needed).
41
47
  # You can also add bots at runtime: "@Sentinel listen to @alertbot for project 1881"