@misterhuydo/sentinel 1.4.59 → 1.4.61

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-25T12:30:48.361Z
1
+ 2026-03-25T13:53:46.873Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-25T12:25:21.116Z",
3
- "checkpoint_at": "2026-03-25T12:25:21.118Z",
2
+ "message": "Auto-checkpoint at 2026-03-25T14:05:08.341Z",
3
+ "checkpoint_at": "2026-03-25T14:05:08.342Z",
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.4.59",
3
+ "version": "1.4.61",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -29,7 +29,7 @@ from .log_parser import parse_all, scan_all_for_markers, ErrorEvent
29
29
  from .issue_watcher import scan_issues, mark_done, IssueEvent
30
30
  from .repo_router import route
31
31
  from .reporter import build_and_send, send_fix_notification, send_failure_notification, send_confirmed_notification, send_regression_notification, send_startup_notification, send_upgrade_notification
32
- from .notify import notify_fix_blocked, notify_fix_applied, notify_missing_tool
32
+ from .notify import notify_fix_blocked, notify_fix_applied, notify_missing_tool, notify_tool_installing
33
33
  from .health_checker import evaluate_repos
34
34
  from .state_store import StateStore
35
35
 
@@ -58,6 +58,93 @@ def _register_signals():
58
58
  pass
59
59
 
60
60
 
61
+ # ── Safe auto-install ─────────────────────────────────────────────────────────
62
+
63
+ # Whitelist: tool command → {pkg name for Claude, build files that prove the project needs it}
64
+ _SAFE_TOOLS: dict[str, dict] = {
65
+ "mvn": {"pkg": "maven", "build_files": ["pom.xml"]},
66
+ "npm": {"pkg": "npm", "build_files": ["package.json"]},
67
+ "gradle": {"pkg": "gradle", "build_files": ["build.gradle", "build.gradle.kts"]},
68
+ "pip3": {"pkg": "python3-pip", "build_files": ["requirements.txt", "pyproject.toml", "setup.py"]},
69
+ "pip": {"pkg": "python3-pip", "build_files": ["requirements.txt", "pyproject.toml", "setup.py"]},
70
+ "yarn": {"pkg": "yarn", "build_files": ["yarn.lock"]},
71
+ "make": {"pkg": "make", "build_files": ["Makefile"]},
72
+ }
73
+
74
+
75
+ def _auto_install_if_safe(
76
+ tool: str,
77
+ repo_path: str,
78
+ sentinel: SentinelConfig,
79
+ repo_name: str,
80
+ source: str,
81
+ ) -> bool:
82
+ """
83
+ Auto-install a missing build tool only when both conditions are met:
84
+ 1. The tool is in _SAFE_TOOLS (known safe, no arbitrary installs)
85
+ 2. Its expected build file exists in the repo (proves the project actually needs it)
86
+
87
+ Posts Slack notifications before (installing…) and after (success/failure).
88
+ Returns True if installation succeeded.
89
+ """
90
+ from .notify import slack_alert
91
+
92
+ spec = _SAFE_TOOLS.get(tool)
93
+ if not spec:
94
+ logger.info("Tool '%s' not in safe whitelist — skipping auto-install", tool)
95
+ return False
96
+
97
+ repo_p = Path(repo_path)
98
+ if not any((repo_p / bf).exists() for bf in spec["build_files"]):
99
+ logger.info(
100
+ "Tool '%s' is whitelisted but no matching build file found in %s — skipping",
101
+ tool, repo_path,
102
+ )
103
+ return False
104
+
105
+ pkg = spec["pkg"]
106
+ logger.info("Auto-installing '%s' (pkg: %s) required by %s", tool, pkg, repo_name)
107
+ notify_tool_installing(sentinel, tool, repo_name, source)
108
+
109
+ env = {**os.environ}
110
+ api_key = sentinel.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "")
111
+ if api_key:
112
+ env["ANTHROPIC_API_KEY"] = api_key
113
+
114
+ prompt = (
115
+ f"Install '{pkg}' on this server so it can be used as a build tool. "
116
+ f"Detect the OS and package manager (yum/dnf/apt), then run the appropriate install command. "
117
+ f"After installing, verify by running '{tool} --version' or the equivalent. "
118
+ f"Report the installed version. No explanations — just install and report."
119
+ )
120
+ try:
121
+ result = subprocess.run(
122
+ [sentinel.claude_code_bin, "--dangerously-skip-permissions", "--bare", "--print", prompt],
123
+ capture_output=True, text=True, timeout=300, env=env,
124
+ )
125
+ output = ((result.stdout or "") + (result.stderr or "")).strip()
126
+ success = result.returncode == 0
127
+ logger.info("Auto-install '%s': %s\n%s", tool, "OK" if success else "FAILED", output[-500:])
128
+ if success:
129
+ slack_alert(
130
+ sentinel.slack_bot_token,
131
+ sentinel.slack_channel,
132
+ f":white_check_mark: *`{tool}` installed* for *{repo_name}*\n"
133
+ f"```{output[-300:]}```\nRetrying fix now...",
134
+ )
135
+ else:
136
+ slack_alert(
137
+ sentinel.slack_bot_token,
138
+ sentinel.slack_channel,
139
+ f":x: *Auto-install of `{tool}` failed* for *{repo_name}*\n"
140
+ f"```{output[-300:]}```\nAdmin intervention required.",
141
+ )
142
+ return success
143
+ except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
144
+ logger.error("Auto-install of '%s' failed: %s", tool, exc)
145
+ return False
146
+
147
+
61
148
  # ── Fix pipeline ──────────────────────────────────────────────────────────────
62
149
 
63
150
  async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: StateStore):
@@ -109,10 +196,19 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
109
196
  try:
110
197
  commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
111
198
  except MissingToolError as e:
112
- logger.warning("Missing tool for %s: %s — notifying admin", event.source, e)
113
- notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, "")
114
- store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
115
- return
199
+ logger.warning("Missing tool for %s: %s", event.source, e)
200
+ if _auto_install_if_safe(e.tool, repo.local_path, sentinel, repo.repo_name, event.source):
201
+ try:
202
+ commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
203
+ except MissingToolError as e2:
204
+ logger.error("Still missing tool after auto-install: %s", e2)
205
+ notify_missing_tool(sentinel, e2.tool, repo.repo_name, event.source, "")
206
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
207
+ return
208
+ else:
209
+ notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, "")
210
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
211
+ return
116
212
  if commit_status != "committed":
117
213
  store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
118
214
  send_failure_notification(sentinel, {
@@ -251,12 +347,50 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
251
347
  cicd_trigger(repo, store, event.fingerprint)
252
348
 
253
349
  except MissingToolError as e:
254
- logger.warning("Missing tool for %s: %s — notifying admin", event.source, e)
350
+ logger.warning("Missing tool for %s: %s", event.source, e)
255
351
  submitter_uid = getattr(event, "submitter_user_id", "")
256
- notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, submitter_uid)
257
- # Archive the issue — admin can re-raise it after installing the tool
258
- store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
352
+ if not _auto_install_if_safe(e.tool, repo.local_path, sentinel, repo.repo_name, event.source):
353
+ notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, submitter_uid)
354
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
355
+ mark_done(event.issue_file)
356
+ return
357
+ # Tool installed — retry apply_and_commit once
358
+ try:
359
+ commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
360
+ except MissingToolError as e2:
361
+ logger.error("Still missing tool after auto-install: %s", e2)
362
+ notify_missing_tool(sentinel, e2.tool, repo.repo_name, event.source, submitter_uid)
363
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
364
+ mark_done(event.issue_file)
365
+ return
366
+ if commit_status != "committed":
367
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
368
+ notify_fix_blocked(sentinel, event.source, event.message,
369
+ reason="Patch was generated but commit/tests failed after tool install",
370
+ repo_name=repo.repo_name, submitter_user_id=submitter_uid)
371
+ mark_done(event.issue_file)
372
+ return
373
+ branch, pr_url = publish(event, repo, sentinel, commit_hash)
374
+ store.record_fix(
375
+ event.fingerprint,
376
+ "applied" if repo.auto_publish else "pending",
377
+ patch_path=str(patch_path), commit_hash=commit_hash,
378
+ branch=branch, pr_url=pr_url, repo_name=repo.repo_name, sentinel_marker=marker,
379
+ )
380
+ send_fix_notification(sentinel, {
381
+ "source": event.source, "severity": "ERROR",
382
+ "fingerprint": event.fingerprint, "first_seen": event.timestamp,
383
+ "message": event.message, "stack_trace": event.body,
384
+ "repo_name": repo.repo_name, "commit_hash": commit_hash,
385
+ "branch": branch, "pr_url": pr_url,
386
+ "auto_publish": repo.auto_publish, "files_changed": [],
387
+ })
388
+ notify_fix_applied(sentinel, event.source, event.message,
389
+ repo_name=repo.repo_name, branch=branch, pr_url=pr_url,
390
+ submitter_user_id=submitter_uid)
259
391
  mark_done(event.issue_file)
392
+ if repo.auto_publish:
393
+ cicd_trigger(repo, store, event.fingerprint)
260
394
 
261
395
  except Exception:
262
396
  logger.exception("Unexpected error processing issue %s — archiving to prevent retry loop", event.source)
@@ -230,6 +230,22 @@ def notify_fix_blocked(
230
230
  logger.warning("notify_fix_blocked: email notification failed: %s", exc)
231
231
 
232
232
 
233
+ def notify_tool_installing(cfg, tool: str, repo_name: str, source: str) -> None:
234
+ """
235
+ Post a brief Slack notice that Sentinel is auto-installing a whitelisted build tool.
236
+ Called before the install begins so admins know what's happening.
237
+ """
238
+ repo_line = f" for *{repo_name}*" if repo_name else ""
239
+ slack_alert(
240
+ cfg.slack_bot_token,
241
+ cfg.slack_channel,
242
+ f":gear: *Auto-installing `{tool}`{repo_line}*\n"
243
+ f"`{tool}` is required to run tests but not found on this server. "
244
+ f"It's a known safe build tool — installing automatically. "
245
+ f"Will retry the fix once done.",
246
+ )
247
+
248
+
233
249
  def notify_missing_tool(
234
250
  cfg,
235
251
  tool: str,
@@ -60,8 +60,7 @@ WORKSPACE_DIR=./workspace
60
60
  # Comma-separated list of Slack bot IDs (starts with B, not U).
61
61
  # SLACK_WATCH_BOT_IDS=B12345678, B87654321
62
62
 
63
- # Boss conversation mode — controls how Boss handles off-topic requests (default: standard)
64
- # standard — professional DevOps agent; off-topic questions answered helpfully but concisely
65
- # strict — DevOps only; politely redirects off-topic requests (recommended for shared/enterprise workspaces)
66
- # fun — fully open; jokes, stories, SVGs, creative tasks — Boss engages enthusiastically
63
+ # Boss mode override overrides the workspace-level BOSS_MODE for this project only.
64
+ # Options: standard (default) | strict | fun
65
+ # See workspace sentinel.properties for full description.
67
66
  # BOSS_MODE=standard
@@ -90,3 +90,10 @@ SYNC_MAX_FILE_MB=200
90
90
  # list_all_errors, export_db
91
91
  # These are powerful operations — restrict to trusted team members.
92
92
  # SLACK_ADMIN_USERS=U123ABC
93
+ #
94
+ # Boss conversation mode — controls how Boss handles off-topic (non-DevOps) requests.
95
+ # standard — professional; off-topic questions answered helpfully but concisely (default)
96
+ # strict — DevOps only; off-topic politely declined (recommended for shared/enterprise workspaces)
97
+ # fun — fully open; jokes, stories, SVGs, creative tasks — Boss engages enthusiastically
98
+ # Can be overridden per-project in config/sentinel.properties.
99
+ # BOSS_MODE=standard