@misterhuydo/sentinel 1.4.60 → 1.4.62
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/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-
|
|
3
|
-
"checkpoint_at": "2026-03-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-25T14:15:51.170Z",
|
|
3
|
+
"checkpoint_at": "2026-03-25T14:15:51.171Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
package/python/sentinel/main.py
CHANGED
|
@@ -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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
350
|
+
logger.warning("Missing tool for %s: %s", event.source, e)
|
|
255
351
|
submitter_uid = getattr(event, "submitter_user_id", "")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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,
|
|
@@ -96,4 +96,4 @@ SYNC_MAX_FILE_MB=200
|
|
|
96
96
|
# strict — DevOps only; off-topic politely declined (recommended for shared/enterprise workspaces)
|
|
97
97
|
# fun — fully open; jokes, stories, SVGs, creative tasks — Boss engages enthusiastically
|
|
98
98
|
# Can be overridden per-project in config/sentinel.properties.
|
|
99
|
-
|
|
99
|
+
BOSS_MODE=standard
|